谈谈我对指针和结构体的理解

为何要学结构体和指针

最近重学C语言版的数据结构,在动手实现前,以为好像和Java也差不了太多,直接上手写了一个最基本的顺序存储的线性表,嗯,有几个语法错误,在编译器的提示下,修正并运行起来,棘手的问题才刚刚开始,segment fault,出现了。数组

segment fault翻译过来也就是段错误属于Runtime Error 当你访问未分配的内存时,会抛出该错误安全

C中关于内存的问题还有内存泄漏(memory leak), 这些问题最终都有可能抛出段错误,或者程序运行无响应,又或在运行结束后返回像这样的一行语句bash

Process exited after 5.252 seconds with return value 3221225477数据结构

而这些内存问题的根源每每是对指针的使用不够恰当,忽略了指针的初始化,或者弄不清楚指针的指向。因为大一学习C语言时,不够用功,对指针与结构体的基础有至关的缺失,为了弥补这些缺失,同时方便后续数据机构的C实现,我决定从新探究一下C和指针。函数

在学习的过程当中也很感谢C和指针这本书,我重点阅读了6,10,11,12四个章节的内容,pdf版我也会放在文末。学习

结构体的定义和使用

struct ListNode {
    int a;
    float b;
    char* c;
};
//未define type前生命结构体变量必须跟上struct关键字
struct ListNode ln = {0};  //初始化,a=0,b=0.000000, c = NULL/0
struct ListNode* p;
//typedef能够省去struct,直接用ListNode声明
typedef struct ListNode ListNode;
ListNode ln; //valid
typedef struct ListNode* PtrToListNode;
PtrToListNode p;  //valid
复制代码

结构体(struct)的用途

  1. 相似面向对象中类的功能,方便访问结构化的数据
  2. 结合指针来实现链表,这也许才是结构体的最普遍的用途了吧,毕竟要用到类的话,为何不选择一门面向对象的语言呢, 所以本文主要强调链表

结构的存储分配,有趣的问题

来看下面这两个结构ui

struct s1 {
	char a;
	int b;
	char c;
};
struct s2 {
	int b;
	char a;
	char c;
};
复制代码

它们的成员域彻底同样,只是生命变量的顺序不同,那么它们的大小呢?spa

printf("s1 size:%d\n", sizeof(struct s1));
printf("s2 size:%d\n", sizeof(struct s2));
复制代码

输出结果:操作系统

s1 size:12
s2 size:8翻译

这就是这两个结构体存储结构的差别致使的,咱们都知道

1个int = 4个字节

1个字符 = 1个字节

  • s1: 先拿1个字节存放a, 而a后面的3个字节空闲,接下来4个字节的b, 最后1个字节的c空闲3个字节,以存放下一个结构体,一共12个字节

  • s2: b占用4个字节,a,c 占用2个连续的字节, 最后空余2个字节,以存放下一个结构体,一共8个字节

指针

1.指针的基本概念

  • 指针也是一个变量
  • 指针的声明并不会自动分配任何内存,指针必须初始化,未清楚指向的先初始化为NULL
  • 指针指向另一个变量(也能够是另外一个指针)的内存地址
  • 指针的内容是它所指向变量的值
  • 指针的值是一个整形数据
  • 指针的大小是一个常数(一般是4个字节,64位操做系统是8个字节,但编译器也许通通默认为32位)
  • 指针的类型(void*/int*/flot*等)决定了间接引用时对值的解析(值在内存中都是由一串2进制的位表示,显然值的类型并不是值自己固有得特性,而是取决于它的使用方式)

例:一个32位bit(4个字节byte)值:01100111011011000110111101100010

类型
1个32位整数 1735159650
2个16位整数 26476和28514
4个字符 glob
浮点数 1.116533 * 10^24

2.指针的使用

指针的使用场景:

  1. 创建链表
  2. 做为函数参数传递
  3. 做为函数返回值
  4. 普通数组同样使用指针
  5. 创建变长数组

1.单链表的创建

struct ListNode {
    int val;
    struct ListNode *next; //注意这里的*,想一想没有*的话该结构体的定义合法吗?
};
复制代码

链表是C语言,数据结构的难点,关于链表的详细问题,我会在下一篇博客中详细解释。

2.何时要用指针做函数参数?

Ans:

  1. 要经过函数改变一个函数外传来的参数的值,只能用址传递,即用指针做为参数传进函数。
  2. 即便不要求对变量修改,也最好用指针做参数。

尤为是当传入参数过大时,例如一个成员众多的结构体,最好用指向该结构体的指针来代替,一个指针最大也就8个字节,不只如此,C语言传值调用方式要求将参数的一份拷贝传递给函数。

所以,值传递对空间和时间都是一个极大的浪费,之后能够看到将指针做为参数的例子将会很常见,址传递惟一的缺陷在于存在函数修改该变量,能够将其设为常指针的方式避免这种状况的发生。

void print_large_struct(large_struct const * st){}

这行语句的做用是,告诉编译器个人st这个指针是一个常指针,它指向的内容不能被改变,若是我在函数不当心改变了它的内容,请报错给我。

只能用指针的例子--调用函数改变a的值

//改变参数的值的两种方式
void changeByValue(int a){
	a = 666;
	printf("in the func:%d\n", a);
}
void changeByAddr(int* a){
	*a = 666;
}
int main() {
	int a = 0;
	
	changeByValue(a);  //a直接做参数
	printf("out the func:%d\n", a);

	changeByAddr(&a); //取a的地址做参数
	printf("after changeByAddr:%d\n", a);

	return 0;
}
复制代码

输出结果:

in the func:666 out the func:0
after changeByAddr:666

3.为何用指针来做为返回值?

Ans:
个人意思是,你有时能够这么作

4.指针与普通数组

  • 指针指向数组
int a[3] = {1, 2, 3};
int* pa = a;
for (int i = 0; i < 3; ++i)
	printf("%d\n", *pa++);
复制代码

*pa++其实是先对pa间接引用,即*pa,再执行pa = pa + 1,注意这里的1不是指针运算上移动一个字节,编译器会根据指针的类型进行移动,例如这里类型,是整型实际上移动4个字节,一个int的长度。

  • int
for (int i = 0; i < 3; ++i)
	printf("%d\n", pa++);
复制代码

输出结果:

6487600
6487604
6487608

  • double
double b[3] = {1, 2, 3};
double *pb = b;
for (int i = 0; i < 3; ++i)
  printf("%d\n", pb++);
复制代码

输出结果:

6487552
6487560
6487568

与此同时,数组变量自己就是指向数组第一个元素也就是a[0]的指针,它包含了第一个元素的地址,所以也彻底能够把a当成一个指针来用,如下的引用都是合法的。

p = a;
p = &a[0]; //与上面相同都是将p指向a数组

//a++不合法,数组名不能做左值进行自增运算,采用*(a+i)的方式推动
for (int i = 0; i < 3; ++i)
	printf("%d\n", *(a+i));
复制代码

5.操做指针来自定义一个变长数组?

写下这一点的我又看了翁恺老师的mooc(c语言进阶),其中的4.1很是的经典,基本是线性表的雏形了。

  • 变长数组
//Q1
typedef struct Array{
	int * array;
	int size;
}Array;
//Q2
Array arrary_create(int init_size){
	Array a;
	a.size = init_size;
	a.array = (int*)malloc(init_size * sizeof(int));
	return a;
}

void array_free(Array* a){
	free(a->array);
	a->array = NULL;
	a->size = 0;
}
//Q3
void array_inflate(Array* a, int more_size){
	int* p = (int*)malloc((a->size + more_size) * sizeof(int));
	for (int i = 0; i < a->size; ++i)
		p[i] = a->array[i];
	free(a->array);
	a->array = p;
	a->size = a->size + more_size;
}
//Q4
int array_at(Array const * a, int index){
	return a->array[index];
}
复制代码

在以上代码,我分别做了4个标记,它们对应着4个问题。

Q1:Why Array not Array* ?

  • typedef struct Array{...}* Array

这么作?我将没法获得一个结构体的本地变量,我只能操做指向这个结构体的指针,却没法生成一个结构体,这是一个好笑的问题,个人指针该指向谁呢? 同时,看到Array a;你能想到a它是一个指针吗?

Q2:Again ?Why Array not Array* ?

Array* array_create(int init_size){
	Array a;
	a.size = init_size;
	a.array = (int*)malloc(init_size * sizeof(int));
	return &a;
}
复制代码

这样作?注意到这个a是在array_create函数里面定义的局部变量哦。让咱们来看一下C的回收机制,你就会明白,为何这样作行不通。

  1. 若是是在函数内定义的,称为 局部 变量,存储在栈空间内。它的空间会在函数调用结束后自行释放。
  2. 若是是全局变量,存储在DATA段或者BSS段,它的空间是始终存在的,直至程序结束运行。
  3. 若是是new或者malloc获得的空间,它存储在HEAP(堆)中,除非手动delete或free,不然空间会一直占用直至进程结束。

函数的确返回了一个指针,但在函数返回的同时,a就会被回收,那么你返回的a的地址就是一个指向未知位置的指针,是一个意义不明确的值,再也不是你所认为的指向那个你当初在函数里创造的结构体了哦。

另外一种作法?

Array* array_create(Array* a, int init_size){
	a->size = init_size;
	a->array = (int*)malloc(init_size * sizeof(int));
	return a;
}
复制代码

这么作不是不能够,但它有两个潜在的风险

  1. 若是a == NULL ,那么这必然引起内存访问错误;
  2. a已经指向了某个已经存在的结构体,那你在新建的是否是要对a->array进行free呢?

与其这样复杂,咱们不妨采用更为简单的办法,返回一个结构体自己。

Q3: 每次inflate都要将原来array里的元素复制到新申请的空间里面太复杂?

固然,你也能够这样作:

void array_inflate(Array* a, int more_size){
	a->array = (int*)realloc(a->array, (a->size + more_size) * sizeof(int));
	a->size = a->size + more_size;
}
复制代码

那么既然都已经接触到malloc,realloc了,不妨在此总结如下这几个函数吧!

malloc calloc realloc 和 free

它们都是从堆上获取可用的(连续?至少逻辑上是连续的,物理上根据操做系统, 极可能不是连续的)的内存块的首地址,返回的都是void*类型的指针,都须要强制类型转换。
它们申请的内存有可能比你的请求略多一点,内存库为空时返回NULL指针。
现实是存在这个可能的!所以用到动态内存分配时必定要检查返回是否是NULL啊

  • realloc与malloc不一样的在于,realloc须要一个原内存的地址,和一个扩大后的size,若是原内存后面接着有可用的内存块,就将这一部分也分给原地址,不然寻找一个足够大的内存,返回新的地址而且自动将数据复制到新的内存
  • calloc第一个参数为申请的个数,第二个参数为每一个单元的大小,例如calloc(100,sizeof(int))申请100个int大小的内存。注意,calloc最大的不一样在于它会自动为这些内存初始化,指针初始化为NULL, 很大程度上避免了一些未初始化的错误。
  • free接受一个指针类型的参数,这个参数要么是NULL,free(NULL)是安全的。 要么就只能是上面三兄弟从堆里分配来的内存了。

Q4: Why const* ?

这算是指针做为函数参数传入的例子了吧,const是由于我不但愿我访问a中元素时,a被修改掉了,因此告诉编译器帮我盯着一下。事实上咱们看到函数里面很安全,并无对*a进行修改,函数足够简单时,咱们彻底能够去掉const

3. 指针的运算

须要注意的是,当指针指向的并非一个数组时,指针的运算是无心义的

//指针是整型的数据,它们之间固然能够运算,但下面是无心义的
int a = 3;
int b = 1;
int* pa = &a;
int* pb = &b;
printf("%d\n", pb - pa);
复制代码

但当指针指向一个数组时,减法运算的意义就是两个指针的距离,这个距离也是一个逻辑上的距离

int a[5];
int *pa, *pb;
pa = &a[0], pb = &a[3];
int distance = pb - pa;
复制代码

获得的distance是16/4(1个int4个字节)为4; 再看一个有趣的例子,指针的关系运算

//让a中元素所有变成5
int a[3] = {1, 2, 3};
int* p;
for(p = &a[0]; p < &a[3]; *p ++ = 5); //长得有点奇怪却合法的for循环
复制代码

a++ 和 ++a的相同点都是给a+1,不一样点是a++是先参加程序的运行再+1,而++a则是先+1再参加程序的运行。

在这里咱们访问了数组最后一个元素后面那个地址,并与之做比较来决定推动的边界, 这竟然是合法的,事实上在最后一次比较时咱们的p已经指向了那个位置,但咱们没有对其进行间接访问,所以这组循环是彻底合法的。 再看下面这个例子:

//将a中元素所有变成0
for(p = &a[2]; p >= &a[0]; p--)
  *p = 0;
复制代码

在最后一次比较时,p已经从a[0]的位置自减了1,也就是说它移到了数组以外,与上一个例子不同的是,他将与a[0]的地址进行比较,这是无心义的,其中涉及到的标准以下:

标准容许指向数组元素的指针与数组最后一个元素后面的那个内存位置的指针进行比较,但不容许与指向数组第一个元素以前的那个内存位置的指针进行比较

关于C中的指针,内容确实太多,在之后的学习中边踩坑,边总结,写做本文的缘由也是在于将本身犯过的错误作一个记录,在总结中积累经验,不断前行,总之,加油吧~

相关文章
相关标签/搜索