C语言指针复习—— 空指针和野指针、数组与指针、指针与函数传参、指针与函数

  

目录

  • 1. 普通指针的理解
    • 1.1 空指针和野指针
    • 1.2 指针的运算
  • 2. 数组与指针
    • 2.1 数组的访问
    • 2.2 字符串数组
    • 2.3 字符型指针
    • 2.4 数组指针与指针数组
  • 3. 指针与函数传参
    • 3.1 数组作为函数形参
    • 3.2 指针作为函数形参
    • 3.3 结构体指针作为函数形参
  • 4. 对于函数的理解
    • 4.1 指针函数和函数指针的区别
  • 5. 结束

  
  

1. 普通指针的理解

  每个变量都有一个符号地址(变量名)和物理地址(在内存中的位置,又叫做指针), 指针变量用来存放普通变量的地址,即指针变量是用来存放普通变量的指针。

1、 指针变量也是一个变量,在内存中也是占内存的

2、 指针变量也有自己的物理地址,其存储是用比该指针类型高一级的指针变量来存放指针变量的地址,如二级指针变量存放一级指针变量的地址

3、当定义了一个指针变量,一定要绑定进行指针绑定,使该指针变量所存放的信息为有效地址
  
  
以下编译代码的环境:// gcc 64位系统

#include 

int main()
{
     
	int    a = 4;
	int*   b = &a;
	int**  c = &b;
	printf("变量a的值为:%d\t地址为:%p\t长度为:%d\n",a,&a,(int)sizeof(a));
	printf("变量b的值为:%p\t地址为:%p\t长度为:%d\n",b,&b,(int)sizeof(b));
	printf("变量c的值为:%p\t地址为:%p\t长度为:%d\n",c,&c,(int)sizeof(c));
	return 0;
}
C语言指针复习—— 空指针和野指针、数组与指针、指针与函数传参、指针与函数_第1张图片

  

1.1 空指针和野指针

  
  在C语言中,如果一个指针不指向任何数据,我们就称之为空指针,用NULL表示;对于下面的a,a = (int *)0;是允许的,而a = 0;是不行的,因为类型不相同,给指针赋初值为NULL,其实就是让指针指向0地址处

 int     *a    =    NULL; 
 /* 以下为NULL的定义
	#ifdef _cplusplus        // 定义这个符号就表示当前是C++环境
	#define NULL 0    // 在C++中NULL就是0
	#else
	#define NULL (void *)0 // 在C中NULL是强制类型转换为void *的0
	#endif
*/

  0地址在一般的操作系统中都是不可被访问的,而指针变量未经定义就解引用时会报错,当一个指针指向的位置是不可知的(随机的、不正确的、没有明确限制的),这个指针称为野指针

#include 
int main()
{
     
	int* a;
	printf("a的值为:%d\n",*a);
	return 0;
}

  

1.2 指针的运算

  指针变量如果是局部变量,则分配在栈上,本身遵从栈的规律(反复使用,使用完不擦除,所以是脏的,本次在栈上分配到的变量的默认值是上次这个栈空间被使用时余留下来的值),就决定了栈的使用多少会影响这个默认值

  指针参与运算时,实际是地址的运算,指针变量+1,并不是真的加1,而是加 1 * sizeof(指针类型);如果是int * 指针,则+1就实际表示地址+4,如果是char * 指针,则+1就表示地址+1;如果是double *指针,则+1就表示地址+8(注意:sizeof是C语言的一个运算符,不是函数,作用是用来返回()里面的变量或者数据类型占用的内存字节数)
  
  

2. 数组与指针

  
  从内存角度讲,数组变量就是一次分配多个相同类型变量,而且这多个变量在内存中的存储单元是依次相连接的;从编译器角度来讲,数组变量也是变量,变量的本质就是一个地址,这个地址在编译器中决定具体数值,具体数值和变量名绑定,变量类型决定这个地址的延续长度

比如定义一个数组 int a[5];
1、a 是数组名,其作为右值表示数组首元素(数组的第1个元素,也就是a[0])的首地址,此时 a 与 &a[0] 是等价的
2、&a 是数组名 a 取地址,实质是一个常量,做右值时表示整个数组的首地址
3、&a 是整个数组的首地址,而 a 是数组首元素的首地址。这两个在数字上是相等的,但是意义不相同,a和&a[0]是元素的指针,也就是int * 类型;而&a是数组指针,是int (*)[5];类型

比如当赋值给一个指针时,就不能用 &a ,而应该用 a 来赋值(此处要注意数组与int型或者其他数据类型赋值地址给指针变量时的区别与不同)

#include 
int main()
{
     
	int a[5] = {
     1,2,3,4,5};
	printf("整个数组 a 的地址为:%p\n",&a); 
	printf("数组a的首元素地址为:%p\n",a);
	
	//int * b = &a; //报错
	int * b = a; //指针变量b应该存放数组首元素地址
	printf("数组 a[2] = %d\t 指针解引用 *(b+2) = %d\n",a[2],*(b+2));
	
	return 0;
}
C语言指针复习—— 空指针和野指针、数组与指针、指针与函数传参、指针与函数_第2张图片

  

2.1 数组的访问

  以指针方式来访问数组元素时,格式为:* (指针+偏移量); 其中的 * 号表示解引用,当指针变量是数组首元素地址(a或者&a[0])时,那么偏移量就是下标;值得注意的是,当一个指针指向的是一个数组时,* (a+1) 所表示的和 a[1] 所表示的时等价的
  

2.2 字符串数组

对于字符串数组 char str[] = “I love you!”;

  str表示的是该字符串第一个字符的地址,即首地址,可以将str看作是指针,所以str + 1, str + 2等用法是合法的,用来获取字符串中某个字符的地址,即 * (str+2) 和 str[2] 是等价的

  字符串数组一旦定义,编译器会将其内容存放在栈,可以通过指针去访问和修改数组内容,访问方式即通过(数组名+下标)或者通过(指针+偏移量),当字符串数组作为右值时,与%s结合可以一次性输出字符串(调用数组名即可)

#include 
//#include 
int main()
{
     
	//字符串数组
	char str1[] = "I love you!"; //当使用双引号时,编译器默认在后面加上\0
	printf("字符串数组的首地址为:%p\n",str1);
	printf("第一个字符的值:%c\t地址为:%p\n",*str1,str1);
	printf("第三个字符的值:%c\t地址为:%p\n",*(str1+2),str1+2);
	
	return 0;
}
C语言指针复习—— 空指针和野指针、数组与指针、指针与函数传参、指针与函数_第3张图片

2.3 字符型指针

对于字符型指针 char* str = “I love you two!”;

  指针所指向的内容存放在静态存储区,即常量区。因此上式的字符型指针str也可以先定义char* str;然后再进行赋值 str = “I love you two!”; 这一点对于字符串数组不是合法的

  str 为字符串中的首元素的地址,与%s结合可以打印全部字符,缺点就是里面的内容不可修改,但是相比于字符串数组存放在栈区,通过字符型指针去访问速度会更快

#include 
//#include 
int main()
{
     
	//字符型指针
	char* str2 = "I love you two!";
	printf("字符串数组的首地址为:%p\n",str2);//即str2变量的值,存放着字符串首地址
	printf("第一个字符的值:%c\t地址为:%p\n",*str2,str2);
	printf("第三个字符的值:%c\t地址为:%p\n",*(str2+2),str2+2);
	
	printf("作为右值可以一次性地赋值:%s\n",str2);
	
	return 0;
}
C语言指针复习—— 空指针和野指针、数组与指针、指针与函数传参、指针与函数_第4张图片

2.4 数组指针与指针数组

  指针数组的实质是一个数组,这个数组中存储的内容全部是指针变量。数组指针的实质是一个指针,这个指针指向的是一个数组

符号 * 与 [] 的优先级:后者更高

① int *p[5]; 等价于int *(p[5]);
② int (*p)[5];

  第一个核心是p,p是一个数组,数组有5个元素,数组中的元素都是指针,指针指向的元素类型是int类型的;整个符号是一个指针数组

  第二个核心是p,p是一个指针,指针指向一个数组,数组有5个元素,数组中存的元素是int类型; 整个符号的意义就是数组指针

  对于指针数组,同一般数组一样,数组名代表首个元素的地址,下面以字符型指针数组举例,注意对比上文的字符型数组与字符型指针来使用;定义一个字符型指针数组,数组元素里都是字符型指针,每一个元素的内容为指向某个字符串的地址值,对数组的元素值进行解引用,就可以得到该字符串

#include 
//#include 
int main()
{
     
	//字符型指针数组
	char *str3[] = {
     "I love you!","I love you two!"};
	printf("字符型指针数组的首地址为:%p\n\n",str3);
	
	//变量str3[0]为字符型指针变量,变量的内容为地址
	printf("str3[0]指向的内容为:%s\n",str3[0]);
	printf("str3[0]字符型指针变量的内容为:%p\n",str3[0]);
	printf("str3[0]指向内容的第三个字符为:%c\n\n",*(str3[0]+2));
	
	printf("str3[1]指向的内容为:%s\n",str3[1]);
	printf("str3[1]字符型指针变量的内容为:%p\n",str3[1]);
	printf("str3[1]指向内容的第五个字符为:%c\n",*(str3[1]+4));
	
	return 0;
}
C语言指针复习—— 空指针和野指针、数组与指针、指针与函数传参、指针与函数_第5张图片

  
  

3. 指针与函数传参

3.1 数组作为函数形参

  数组名作为形参传参时,实际传递的是数组的首元素的首地址(也就是整个数组的首地址),在子函数内部,传进来的数组名就等于是一个指向数组首元素首地址的指针;这种特性叫做“传址调用”,此时可以通过传进去的地址来访问实参
  1、数组作为函数形参时,[] 里的下标数字是可有可无的
  2、数组做函数形参时,也可以使用指针作为形参代替,即使没有数组的长度信息,只要偏移量不超过实际长度,都是合法的
  3、在子函数内部,也可以直接对外部的数组实参作修改

#include 
void test1(int a[])
{
     
	printf("test1:a[1]的值为:%d\n\n",a[1]);
}

void test2(int b[5])
{
     
	printf("test2:a[1]的值为:%d\n\n",b[1]);
}

void test3(int* c)
{
     
	printf("test3:a[1]的值为:%d\n\n",*(c+1));
}

int main()
{
     
	int a[5] = {
     1,2,3,4,5};
	printf("main:a[1]的值为:%d\n\n",a[1]);
	test1(a); test2(a); test3(a);
	
	return 0;
}
C语言指针复习—— 空指针和野指针、数组与指针、指针与函数传参、指针与函数_第6张图片

3.2 指针作为函数形参

  在上面的demo里的test3函数,就演示了,和数组作为函数形参是一样的,这就好像指针方式访问数组元素和数组方式访问数组元素的结果一样是一样的

3.3 结构体指针作为函数形参

  结构体一般都很大,如果直接用结构体变量进行传参,那么函数调用效率就会很低。(因为在函数传参的时候需要将实参赋值给形参,所以当传参的变量越大调用效率就会越低)

  可以使用指针来传参,效率将大大提高;需要注意一点,当指针作为形参时,比如struct Student *st,如果使用st.name点的方式访问成员,会报错,需要使用解引用的方式或者->的方式,如果使用解引用,则要注意结构体对齐方式,可以看我的上一篇博文 C语言结构体的内存存储方式和字节对齐

#include 
struct Studet{
     
	int age;
	char sex;	//结构体对齐
};
void test(struct Studet *st)
{
     
	printf("st的大小为:%ld\n",sizeof(st));
	printf("age of st: %d\n\n",st->age);
}

int main()
{
     
	struct Studet student = 
	{
     
		.age = 16,
		.sex = 'G',//gcc编译器里面的初始化写法
	};
	
	printf("Studet的大小为:%ld\n",sizeof(student));
	printf("age of studet: %d\n\n",student.age);
	
	test(&student);
	
	return 0;
}

  通过上面的展示可以看出,如果要在一个子函数里面来改变传进来的实参赋给形参的值(也就是主函数里面的变量值),采用指针的方式就能解决这个问题,效率高

  
  

4. 对于函数的理解

  函数名是一个符号,表示整个函数代码段的首地址,实质是一个指针常量,如果没有形参列表和返回值,函数也能对数据进行操作,用全局变量即可;但是数参数传参用的比较多,因为这样可以实现模块化编程,而C语言中也是尽量减少使用全局变量

  如果参数很多,通常的做法是把很多参数打包成一个结构体,然后传结构体变量指针进去;此时可以使用const来修饰形参,用法是const int *p;(意义是指针变量p本身可变的,而p所指向的变量是不可变的),以此来声明在函数内部不会改变这个指针所指向的内容

  在典型的linux风格函数中,返回值是不用来返回结果的,而是用来返回0或者负数用来表示程序执行结果是对还是错,是成功还是失败;当函数需要返回多个值时,可以使用指针传参的方式把数据的地址传进子函数中,并且对处理数据的结果加以判断,如果操作成功,则改变指针所指向数据的内容,并且返回成功的信息

#include 
struct B{
     
	int x;
	int y;
	short z;
}b = {
     1,1,1};

int test(int a , struct B *p)
{
     
	a = a*5;
	if(a>30) return -1;
	else{
     
		p->x = a;
		p->y = a+1;
		p->z = a+2;
		return 0;
	}
}
int main()
{
     
	if(0 == test(4,&b))
	{
     
		printf("成功!\n\n");
		printf("x = %d\n",b.x);
		printf("y = %d\n",b.y);
		printf("z = %d\n",b.z);
	}
	else{
     
		printf("错误!\n");
	}
	
	return 0;
}
C语言指针复习—— 空指针和野指针、数组与指针、指针与函数传参、指针与函数_第7张图片

4.1 指针函数和函数指针的区别

符号 () 和符号 * 的优先级:前者最大

① int *fun(int x);
② int (*fun)(int x);

通过判断,可以知道①为指针函数,②为函数指针

  • 指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针

注意:在调用指针函数时,需要一个同类型的指针来接收其函数的返回值

#include 
#include 

typedef struct Data{
     
	int a;
	int b;
}Date;  //这里的意思是把结构体类型重新命名成Date

//指针函数
Date * fun(int a,int b)
{
     
	Date* date = (Date *)malloc(sizeof(Date)) ;
	date->a = a;
	date->b = b;
	return date;
} 

int main(void)
{
     
	Date *myDate = fun(2,3);
	printf("myDate->a=%d\tmyDate->b=%d\n",myDate->a,myDate->b);

	return 0;
}
  • 函数指针,其本质是一个指针变量,该指针指向这个函数。总结来说,函数指针就是指向函数的指针,需要把一个函数的地址赋值给它,有两种写法:
    1、fun = &Function;
    2、fun = Function;
#include 
#include 

int add(int x,int y)
{
     
	return (x+y);
}

int sub(int a,int b)
{
     
	return (a-b);
}

//函数指针 
int (*fun)(int x,int y);

int main(void)
{
     
	fun = add;
	printf("the (*fun)(2,3)is %d\n)",(*fun)(2,3));
	
	//另外一种写法:
	fun = ⊂
	printf("the (*fun)(7,3)is %d\n)",(*fun)(7,3));

	return 0;
}

  
  

5. 结束

  如有错误,还望指正!
  
  
  

你可能感兴趣的:(C/C++语言,c语言)