C指针和数组深度汇总

在看完基本关于C语言的书籍后,自认为C语言已经学得差不多的笔者做了几次面试题,嗯,怀疑人生。而正如大多数人所认为的,C语言的精髓在于指针。 那么就认真罗列一下笔者曾经掉过的坑。

问题1(柔性数组)

struct A{
	int i;
	char str[];
};

在上面结构体中,str 的数组长度为0。 这在vs2012等编译器中会报错。且许多关于C语言的书籍都一再强调,结构体中的数组必须声明长度,但在C99中这是可以被允许的。
这个问题的典型案例是在Linux C的网络文件库中,sys/un.h 头文件中,有关于套接字结构 sockaddr_un 的定义。

/*	一部分头文件版本 	*/
struct sockaddr_un{
	sa_family_t sun_family;
	char sun_path[];
};

/*	川一的Linux 版本	CentOs-7	*/
struct sockaddr_un{
	__SOCKADDR_COMMON (sun_);
	char sun_path[108];
};

但在这类结构体中,柔性数组之前必须有结构体成员。具体用法了解:C99文档

问题2(不同存储类型数组作用域)

int *func(){
	int arr[3] = {1,1,1};
	return arr;
}

int main(){
	int i = 0;
	int *p = func();
	while( i++ < 3 ){
		printf("%d\n",*p++);
	}
	return 0;
}

这段代码的结果是什么?
C指针和数组深度汇总

在linux 环境下使用gcc编译,答案不是三个1,而是无法执行。为什么? 因为arr数组的定义在func函数内部,在主函数执行进入func函数时,系统开辟一块内存让func函数使用,这块内存的“有效时间”就是从函数执行到函数结束所经历的时间。 在func函数这块内存中,arr数组开辟了一块内存存储数组变量。 此时arr数组的作用域仅限于func函数内部。 因此在func函数执行完毕后,arr数组的内存也自然丢失了。因此编译器会报错。

那么如果把func 函数稍作修改呢?

int *func(){
	static int arr[3] = {1,1,1};
	return arr;
}

或者变成这样:

int *func(){
	int *arr = (int*)malloc(3 * sizeof( int ));
	arr[0] = 1, arr[1] = 1, arr[2] = 1;
	return arr;
}

程序还会无法执行么?
不会。

因为static 变量和全局变量在静态地址,只有在程序执行结束才会释放地址。 而动态分配内存不是在栈,而是堆。因此只要不使用free来释放arr数组或者程序结束,这块地址会一直存在。这个东西在后面初始化顺序表,队列等数据类型时会有很大作用。

问题3(初始化动态数组与静态数组)

在进行有些数值计算时候,会需要对数组进行初始化,让数组内部元素是一个确定值。

/*  动态数组 */
void func(){
	int i = 0;
	int *arr = (int*)malloc(3 * sizeof(int));
	while( i < 3 ){
		*arr = i++;
		arr++;
	}
}

/* 静态数组 */
void func(){
	int i = 0;
	int arr[3];
	while( i < 3 ){
		*arr = i++;
		arr++;
	}
}

这两个函数有执行的结果一样么?
不同。

使用动态开辟的数组直接使用数组名进行地址的自加运算是可以被允许的,而静态开辟的数组则不允许直接对数组名进行运算。但是对静态地址可以这样做:

void func(){
	int i = 0;
	int arr[3];
	int *p = arr;
	while( i < 3 ){
		*p = i++;
		p++;
	}
}

使用这种方法也可以让静态数组按指针方法初始化。

虽然动态数组相对比较方便,但是一不注意也会有大坑。

int func(){
	int arr[3];
	int *p = a;
	while( p < &a[2] ){
		*p++ = 0;
	}
}

int func(){
	int *p = (int*)malloc(3 * sizeof(int));
	while( p <= &p[2] ){
		*p++ = 0;
	}
}

为什么要使用这种方法,因为这样效率高。计算机内部变量是通过“寻址”的方式进行操作,在这种情况下,毫无疑问直接用地址进行数组边界的判断效率更加高。 在这个函数中,p是一个三个int 长度的数组,它的起始地址是&p[0] 也就是 p , 终止地址是 &p[3] 。
按照这样的规则,那么arr和p数组都将被初始化0。 是这样么?

静态数组的方法可以获得想要的结果,而动态呢? 这是一个无限循环。 因为p的地址不断向“前”进,p[2]的含义是p的后面第二个地址。意思就是p的地址在不断“前进”,p[2]的地址也在不断“前进”。 想要皮面这种情况笔者觉得只能让动态数组的终止地址不变,再通过“锁定终止地址”来达到初始化目的。

但一般初始化数组(无论哪种类型数组),笔者一般都使用memset这个函数来进行。

静态数组和动态数组都可以使用数组下标的方法来赋值或进行自运算,但指针方法则只有动态数组可以。

问题4(typedef 对指针的使用)

学过C语言的人对于结构体的定义着实是烦躁,因为在使用结构体类型创建数据结构时,很不方便,不如看下面的东西。

struct InfomationStudent{
	int number;
	char *name;
};

例如链表,若要创建新结点,动态开辟内存是要写很多,
struct InfomationStudent*stu = (struct InfomationStudent *)malloc(sizeof(struct InfomationStudent));
只是写这么一条语句,半条命都没了。因此通常会使用 typedef 来简化类型,关于 typedef的具体用法川一没赘述。来看一个问题,

typedef Struct InfomationStudent* InfoStu;
const InfoSu stu1;		/* A */
InfoSu const stu2;		/* B */

上面的第二条,第三条语句有什么不同?
学过const 和 typedef 用法,也许说A中修饰的是stu1,而B中修饰的是指针。 不是,这两条语句时一个东西。就像 const int a ;和 int const a; 没有任何区别一样。 为什么? 因为,typedef 的机制是直接对类型替换,而不是和define 一样的简单文本替换。

问题5(指针使用位置)

前面的例子中,我们稍微做一下修改:

int i = 0;
int arr[3];
int *p = arr;
while( p < &a[2] ){
	*p++;
}

或许你可以轻易地回答出,*p++ 的含义,但是这个呢?

int neg, a = 1 , b = 1;
int *p = &b;
neg = a/*p;

neg 的结果是什么?是1吗? 很遗憾,不是。 这个语句无法执行,
注意看,/* ,这个东西是什么? 注释,a后面的内容全被当做注释注释掉了。 当然在编译器中你会清楚观察到这个问题,但是错了的话,说明你的C语言指针和注释基础还是有很大问题。

问题6(数组地址问题)

许多人在对数组和指针进行了初步的了解后,就以为自己已经完全掌握了它们的用法,那不如看看下面这个很经典的例子。

int a[5] = {1,2,3,4,5};
int *ptr = (int*)(&a+1);

*(a+1), *(ptr-1) 的结果分别会是什么?

搞懂这个问题之前,再熟悉一遍,什么是指针?什么是数组? 指针不是数组,数组也不是指针。 即使将数组成员地址赋给指针,指针也依然只是指针。

当然,你也要搞清楚**&a**,&a[0],a的区别。

&a,是数组的起始位置,而&a[0]是数组首元素的起始位置,至于a,在一维数组中,它和&a[0]的作用是一样的。虽然上面三个东西的地址是一样的,但是你依然要清楚它们的用法与区别。

至于这道题,答案是2,5。
那么接下来这个呢?

int a[5] = {1,2,3,4,5};
int (*ptr)[5] = &a;
int (*pt)[5] = a;

ptr 和 pt 的用法,哪个正确?

毫无疑问是ptr ,具体的区别前面已经讲过,至于对pt的用法,编译器会出现报警,因为类型不匹配。

ptr+3,pt+4的值是多少?若你搞懂了这个,看下面的:

int a[5] = {1,2,3,4,5};
int (*ptr)[3] = &a;
int (*pt)[3] = a;

此时的ptr+3,pt+4值是多少?(大多数编译器会对这个问题报错,但是身为程序员最好要知道这个问题)

问题7(指针符号优先级)

在川一刚上大一学C语言,被指针数组,数组指针,指针函数,函数指针所迷惑。 当然从文字可以区别它们的含义,但是定义呢。

int *a[3];
int (*a)[3];
int *fun();
int (*fun)();

很多初学者都会懵逼,但其实只要清楚运算符的优先级问题就会轻易得出,第一个是指针数组,第二个是数组指针,第三个是指针函数,第四个是函数指针。 因为[],() 符号的运算优先级比* 高,因此变量名首先与优先级高的运算符结合,再与运算顺序低的运算符结合。

int *(*a)[6];
int *(*fun())();
int (*(*fun)())[6];

那么这几个是什么呢?

问题8(地址的强制转换)

若自信自己已经熟练掌握C的强制类型转换用法,那么:

int a[4] = {1,2,3,4};
int *ptr = (int*)(&a+1);
int *pt = (int*)( (int)a + 1);
printf("%x,%x\n",ptr[-1],*pt);

输出的结果是什么?

经过上面的例子,你可能很容易能知道ptr[-1]的地址就是a[3]的地址。 那么*pt呢?

在川一的gcc环境下,运行结果是这样:
C指针和数组深度汇总
pt 的地址,很明显,是a[0]的第二个字节的地址,而一个int又四个字节构成。 能理解了没?

问题9(二维指针,二维数组1)

关于二维指针的基础用法在此不做过多的赘述,直接从二维数组说起。

在多维数组函数之间传递时,若直接使用如下用法,显得业余又累赘,运行效率也大打折扣。因为掌握多层指针对多维数组的使用就显得异常重要。超过二维的数组很少用,因此只讲二维指针和数组。

假设现在需要进行矩阵运算,而你矩阵的计算,运算和显示都在不同函数中,你会怎么做?

/* Method 1 */
void init( int a[3][3] ){
	for(int i = 0 ; i < 3 ; i++){
		for(int j = 0 ; j < 3 ; j++){
			a[i][j] = 0;
		}
	}
}

/* Method 2 */
void init(int **a,int len){
	for(int len = 0 ; i < len ; i++){
		*p++ = 0;
	}
}

或许看上面的代码你会觉得直接传入数组会更方便,但接下来你就会清楚哪种方法更合适一些。

在C语言中,函数是一块连续的内存区域,编译器在对函数进行操作时,不只会对函数内定义的变量开辟空间,也会对传入的参数开辟空间。 此时若使用第一种方法,程序执行的内存开销将非常大,而且指针的位运算的执行效率会比数组的执行高很多。

我们简单从这段代码不同的汇编指令数量来看:
C指针和数组深度汇总
C指针和数组深度汇总

其中第一张是按照第一种方法的汇编指令,反之第二种。
虽然这张图片差距不是很大,但也可以说明效率问题。但要记住一句话:

当数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素地址的指针。 且,使用这种方法无法改变传入数组的值,传入的参数只是一份拷贝。这就是传值调用和传址的区别。

问题10(二维指针,二维数组2)

如何创建一个动态二维数组?
其中有两种办法,一种是通过行指针数组来创建,另一种是通过二维指针来实现。两种方法各有优劣,其中行指针数组这种方法在创建广义表和十字链表的实现中会使用,而二维数组则显得更有趣一点。

行指针创建
int *arr[3];
for( int i = 0 ; i < 3 ; i++){
	arr[i] = (int*)malloc( 3*sizeof(int) );
}

这种方法较为容易理解,毕竟和创建一维数组一样。

二维数组创建
int **arr;
arr = (int**)malloc( 3 *sizeof(int) );
for( int i = 0 ; i < 3 ; i++ ){
	*arr++ = (int*)malloc( 3*sizeof(int));
}
/* Destroy */
for( int i = 0 ; i < 3 ; i++ ){
	free(*arr);
	*arr++;
}
free(arr);

因为是动态开辟,因此在使用完需要释放内存,释放内存从低位向高维拓展。

问题11(开辟动态数组失败)

void fun(char *str , int len){
	str = (char*)malloc( len * sizeof(int) );
}

int main(){
	char *str = NULL;
	fun(str,10);
	strcpy(str,"hello");
	return 0;
}

C指针和数组深度汇总
str的结果会是hello么? 遗憾的是,不是。 此时就是前面讲的传递a和&a的区别。 回想一下字符串输出的时候,是不是printf("%s\n",str); 如果是,那么说明编译器将str也同样当做参数,那fun中的动态开辟就是给str的拷贝,fun中的str开辟了内存。

那么怎么办? 两种办法。

返回值办法

此种办法川一在数据结构创建头结点,头指针的时候经常会用到。

char *fun(char str , int len){
	str = (char*)malloc( len * sizeof(int) );
	return str;
}

int main(){
	char *str = NULL;
	str = fun(str,10);
	strcpy(str,"hello");
	return 0;
}
二维指针办法
void fun( char **str , int len ){
	*str = (char*)malloc(len*sizeof(char));
}

int main(){
	char *str = NULL;
	fun(&str,10);
	strcpy(str,"hello");
	return 0;
}

注意,这里的参数是&str而不是str, 这样传入的是地址而不是值,在函数内部通过“*”来开锁。 这种用法在Linux进程或者线程管理会经常用到。

问题12(函数指针)

说起函数指针很多人就明白:void (*fun)(); 这种格式,那么下面的东西呢?

char *(*fun1)(char *p , char *s);
char **fun2(char *p , char *s);
char *fun3(char *p , char *s);

这三者有什么区别?

按照前面例子讲的,按照运算符优先级来判断。很明显,第一个是一个函数指针,而函数的返回值类型是char *类型。
fun2是一个指针函数,返回值是个二级指针。
fun3是一个指针函数,返回值是一个char指针。

那么接下来的:

void fun(){
	printf(""fun\n);
}

int main(){
	void (*p)();
	*(int*)&p = (int)fun;
	(*p)();
	return;
}

*(int*)&p = (int)fun 表示什么?

通过C语言生成的汇编指令可以知道,函数名的地址就是内存为函数存储内存的首地址。 对于这条语句,这么理解:
首先,p是一个函数指针,p指向一个函数,这个函数的类型和返回值都是void。
&p是指针本身的存储地址。
(int*)&p是将地址强制转换成int类型的指针。
(int)fun 是将函数的入口地址强制转换成int类型。
那么整条语句的作用就是,将函数的入口地址赋给p指针。

(*p)(),就是对指针的调用。
当然,回调函数的使用普遍是这样的:

void fun(){
	printf(""fun\n);
}

int main(){
	void (*p)() = &fun;
	(*p)();
	return;
}

C指针和数组深度汇总

或许你觉得直接调用函数更加快捷便利,但是事实上这种方法不利于全局的开发。 而使用指针来调用函数更有利于模块化的实现。

等等,这儿还有一个要思考的问题:
(*(void (*))0)() ,是什么?

问题13(函数指针数组)

既然上例提到的C的模块化设计,在进行中,大规模软件设计时,不可能总是靠ctags来判断架构,为了更好地使用函数,让函数的使用更加有组织,有系统,可以将一个模块,或者相似功能的函数指针放在一个数组中。

void fun1(){
	printf(""fun1\n);
}
void fun2(){
	printf(""fun2\n);
}
void fun3(){
	printf(""fun3\n);
}

int main(){
	void (*pArr[3])();
	pArr[0] = &fun1;
	pArr[1] = &fun2;
	pArr[2] = &fun3;
	pArr[0]();
	pArr[1]();
	pArr[2]();
}

大致用法就是这样。
那么函数指针数组指针呢?

char *fun1(char *s){
	puts(s);
	return s;
}

char *fun2(char *s){
	puts(s);
	return s;
}

char *fun3(char *s){
	puts(s);
	return s;
}

int main(){
	char *(*a[3])(char *s);
	char *(*(*ptr)[3])(char *s);
	ptr = &a;
	a[0] = &fun1;
	a[1] = &fun2;
	a[2] = &fun3;
	ptr[0][0]("fun1");
	ptr[0][1]("fun2");
	ptr[0][2]("fun3");
	return 0;
}

要时刻对*的级别和对符号的优先级有清楚地认识,这样指针和数组也不是很难理解。 因为实际操作中不会有这么麻烦的用法,但在出现问题时要清楚这个东西是什么。

问题14(malloc)

在开辟动态数组或者其他数据类型时,会经常使用malloc函数。 Linux下,这个函数的声明在/usr/include/stdio.h 中是默认,但在/usr/include/malloc.h ,中则是另一个用法。具体在这不说明。

int *ptr = NULL;
ptr = (int*)malloc(0);
if( ptr == NULL ){
	printf("NULL\n");
}

ptr是0么? printf不会执行,而且ptr会被分配一块内存。
C指针和数组深度汇总

这块地址实实在在存在,也可以进行赋值。
其实在。C Reference malloc函数的介绍中,说明了这个情况。
但是没有人会这么用,因此当做了解内容即可。