C语言——7-指针
0 变量的问题
int a = 5; 属性 : 变量名 变量的存储单元 : 系统会为每个存储单元分配一个唯一的编号,这个编号就是变量的地址. 变量的值 : 变量存储单元中的内容. 访问变量 : a = 1024;//把数值1024存储到 变量a的地址中去 b = a;// 把变量a的值 赋值给b ==> 在C语言中,任何一个变量,都有两个意思 : (1) : 变量的左值 : 变量的地址 (2) : 变量的右值 : 变量的值 对变量的访问 : read 读取变量的值, 从 变量的地址中 取值 write 赋值 把一个数值写到 变量的地址中去. 通过上面的分析, 是不是 只要知道变量的地址 就可以读取/写入 变量了呢? yes 通过变量的地址去访问变量 : 指针
1 变量的访问方式
(1) 直接访问
通过变量名去访问变量.
"直接访问的方式" 受作用域的限制.
(2) 间接访问
通过变量的地址去访问变量."指针"
"不受作用域的限制" :只需要知道你的地址,并且这个地址是可以访问的.
那么你就可以通过地址去访问他.
2 什么是指针呢?
在程序的运行期间,系统会为每个对象在存储器中, 分配一段内存空间.
存储单元 , 按字节大小给每个存储单元分配一个唯一的编号,这个编号
称为存储单元的地址.
指针的概念和地址差不多,一个对象的地址,也可以称为一个对象的指针.
每一个对象都会有一个地址, 或 指针.
地址就是一个编号,这个编号是非负整数.
3 取地址的运算符
& 单目运算符 &x ---> 取对象x的地址 这个表达式的值就是 "对象x的地址(整数形式的值, 非负数)" int a = 5; int b = 5; printf("%p\n",&a);//%p 打印对象的地址, 十六进制输出 printf("%p\n",&b); scnaf("%d",&a);
4 指针运算符
* 单目运算符, 接一个"地址" *地址 <==> 地址对应的那个对象例子 :
int a =5; *(&a) : * 后面加一个 "地址" : 变量a的地址 *(&a) ==> 在任何时候,都等价于 地址对应的那个对象 a *&a = 1024;// a = 1024*& 可以直接约掉 *&a ==> a &*&a ==> &a注意
*地址 等价于 地址对应的那个对象.
不是对应的只是变量的值,也不是对应的只是变量的地址.
因为那个对象,在不同的上下文, 有可能是左值,也有可能是右值.
#include <stdio.h> int main() { int a = 5; int b; // *&a = 1024;// ==> a = 1024 // printf("%d\n",a); // printf("%d\n",*&a); b = *&a;// ==> b = a; printf("b = %d\n",b); return 0; }
5 指针变量
指针变量是什么? B
A 指针 B 变量
指针变量就是一个变量, 只不过这个变量保存的是另一个对象的地址(指针).
指针变量和普通变量有一点不一样 : 这个东西保存的是另外一个对象的地址.
指针变量如何定义呢?
普通变量的定义 : 变量的类型 变量名; 如 : int a;"指向"
如果 p 保存了a的地址,那么我们说 : p 指向了a
保存了谁的地址就指向谁
例子 :
int a =5; p = &a;//把a的地址, 写入到变量p的存储空间中去 //p 保存的是 a的地址 p 指向 a p是一个指针变量 p指向的类型是什么? p指向的类型是指 : p指向的对象的类型 type(a1) : int指针变量的定义 :
指向的类型 *指针变量名;int a; int *p;//定义了一个指针变量p,指向一个 int类型的变量 p = &a; //int *p = &a; //指针变量定义时就赋初值 p 是一个变量,C语言中, 任何变量都有左值和右值 : 请区分 : p的左值是什么? p本身的地址, &p p的右值是什么? p存储空间的内容 , &a p = xxx;//p代表的是左值 y = p;//p代表的是p的右值 *p : p代表的是p的左值,还是 p的右值呢? p的右值指针变量的使用 :
p = &a;//把 a 的地址,存入到 p的存储空间中. p 指向 a p的左值 是p本身的地址 &p p的右值 p存储单元的内容, &a *p ==> *&a ==> a *p p指向的对象例子 :
int a = 5; int *p; p = &a; *p = 250; a = 1024;#include <stdio.h> int main() { int a = 5; int *p; p = &a; //p的右值 是 p存储单元的内容,&a //p的左值 是 p本身的地址 // int *p = &a;//定义了一个指针变量p,同时初始化p // // ==> int *p; p = &a; // printf("%p\n",p); // printf("%p\n",&a); // printf("%p\n",&p); * 指针运算符, 后面可以接一个地址, * 地址,代表是地址对象的那个对象 * &a = a; *a a此时是a的右值, a的右值是一个整数值,不是地址. *a是有问题的. //*p : p代表的是p的右值, p的右值是一个地址 // *p ==> * &a ==> a *p = 1024;//a = 1024 int b = *p; printf("a = %d\n",a); printf("b = %d\n",b); return 0; }
6 野指针 与 空指针
6.1 野指针
野指针是一个指向未知(undefine)的 , 不确定地方的指针.
"未知的,不确定的" , 指向的地方可能存在,可能不存在.
可能可以访问,也可能不可以访问.
对野指针的访问,会有后果?
可能可以访问,可能不可以访问(导致非法的内存访问).
非法的内存访问
不存在的地方,你去访问
存在但不能写,你去写
存在但不能读,你去读
后果 : 非法的内存访问,会导致 "segmentation fault段错误" , 系统把你的进程给kill掉.
例子 :
1. int *p; //定义了一个指针变量p , 你没有赋初始值,不代表p没有值,相反p一定会有一个值. //意思是p一定会指向一个地方,但是这个值是多少,指向哪里, //你是不知道的 undefine(未知的,未定义的) 请问 p 是不是一个野指针? A A 是 B 不是 2. int *p = 5; //p是一个指针 指向一个地址为5的地方,p是一个指向确定的地方 //p不是野指针, 尽管 5 这个地址可能不存在 请问 p 是不是一个野指针? B A 是 B 不是 3. int *p; int a; ... p = &a; 请问 p 是不是一个野指针? A 是 B 不是
6.2 空指针
空指针是一个指向空(不存在的地方,NULL)的指针.
空指针不是野指针,因为空指针指向了一个 确定的地方(尽管这个地方不存在).
对空指针的访问,一定会导致 "非法的内存访问(段错误)"
int *p = NULL;//p 是空指针 *p //用*p 就会出现段错误.
注意
不能对野指针进行操作, 这种行为是很危险. 宁可使用空指针,也不可使用野指针.
在定义指针的时候,可以让指针指向空的地方.
接下来, 我们来看下面的代码 :
int a; int *p = &a; //指针p, 指向a //假设 &a : 0x3000 printf("%p\n",p);//0x3000 printf("%p\n",p+1);//0x3001?
7 指针做加减
a + b a - b加号+ 减号- 只要求 a ,b 是一个数(整数,浮点数)
a,b可以是一个指针,指针的值本质是一个地址编号(非负整数)
指针是可以加减的, 指针加减一个整数, 表达式的类型还是原指针的类型.
例子 :
int a; int *p;//p 是一个指向 int 类型的指针 //typeof(p) : int * typeof(a) : int typeof(p) : int * typeof(*p) : int p是一个指针, *p 表示p指向的那个对象 typeof(*p) : p指向的那个对象的类型 typeof(p) : int* typeof(p + 1) : int * typeof(p - 1) : int * double *p2; typeof(p2) : double* typeof(p2 + 1) : double*#include <stdio.h> int main() { // int a; // int *p = &a; // //指针p ,指向a // //假设&a : 0x3000 // printf("&a = %p\n",&a);//0x3000 // printf("p = %p\n",p);//0x3000 // printf("p + 1 = %p\n",p + 1);//0x3004 double b; double* p = &b; printf("&b = %p\n",&b);//0x3000 printf("p = %p\n",p);//0x3000 printf("p + 1 = %p\n",p + 1);//0x3008 return 0; }
指针作加减 p + i(p 是一个指针, i 是一个整数)
不是简单的加减数值,而是 加减i个指向单元的长度
p + i的值是在p的值后面, 挪 i 个指向类型(typeof(*p)) 的长度
p 指向一个int p + 1 指向下一个int ... p指向一个double p + 1 指向下一个double练习 :
(1) 分析如下程序的输出结果
int main() { double a; int *p; p = (int*)&a; printf("p = %p\n",p); printf("p + 1 = %p\n",p + 1); return 0; }思考 : 两个指针可不可以做加减? 同类型指针
int *p; int *q; p - q p + q ? //没有意义
8 数组元素的地址
int a[10];//系统会为a开辟一段连续的内存空间,一次存放数组中的每个元素 a[0] a[1] a[2] ... |-----------|-----------|------------|... 0x3000 0x3004 0x3008 ... &a[0] : 元素a[0]的地址 : 0x3000 typeof(&a[0]) : int * &a[0] + 1 : &a[1] : 0x3004 &a[0] 的值是 : 0x3000 typeof(&a[0]) : int * &a[0] + 1 ==> 指针 + 1 ,往后挪一个int &a[0] + 2 : &a[2] ... &a[0] + i : &a[i] ... &a[3] + 2 : &a[5] ... &a[x] + y : &a[x + y] ... &a[0] + i ==> a[i] *&a[0] ==> a[0] &a[x] + y ==> &a[x+y] *(&a[x] + y) ==> *(&a[x+y]) ==> a[x+y]练习 :
(1) 分析如下程序的输出结果
int a[10] = {0,1,2,3,4,5,6,7,8,9}; int *p; p = &a[3]; printf("%d\n",*p);//3 printf("%d\n",*(p + 3);//6 printf("%d\n",*(&a[3] + 3));//6#include <stdio.h> int main() { int a[10] = {0,1,2,3,4,5,6,7,8,9}; *(&a[3] + 2 ) = 250;//*&[5] = a[5] printf("%d\n",a[5]); printf("%d\n",*(&a[0] + 5)); printf("%d\n",*(&a[4] + 1)); for(int i = 0;i < 10;i++) { printf("%p\n",&a[i]); } printf("-----------------------\n"); for(int i = 0;i < 10;i++) { printf("%p\n",&a[0] + i); } return 0; }
9 数组与指针
数组名可以当指针来用的
结论
数组名可以当指针(指针常量)来用
数组名当指针用时,数组名可以看作是指向数组第一个元素的指针常量.
如 :
已知 a 是一个数组名(任何类型的数组,任何维度的数组,都可以),
a 当指针用时, &a[0]
数组名即可以代表整个数组,又可以当指针用,
那他什么时候代表整个数组,什么时候当指针用呢?
假设 a 是数组名
- (1) 在如下情况下 , a代表整个数组
sizeof(a) : 求数组a 的所占的字节数,此时a 代表整个数组 typeof(a) : 求 a 的类型时, 此时 a 代表整个数组. &a : a的地址,此时a代表整个数组, 整个数组的地址.
- (2) 其他情况下, 当指针用
p = a;//a 当指针用,why? 不能把整个数组赋值给其他对象 //a 当作指针 p = a; ==> p = &a[0]a + 1;//a当作指针用,why? 整个数组不能整体 +1 a + 1 ==> &a[0] + 1 ==> &a[1] ...例子 :
int a[10]; a + 1 //在这个表达式中 , 数组名 a 当作指针来用 //a + 1 ==> &a[0] + 1 ==> &a[1] a + i // //a + i ==> &a[0] + i ==> &a[i] *(a + i) ==> *(&a[0] + i) ==> *&a[i] ==> a[i]结论
*(p + i) <===> p[i] ,when i >= 0练习 :
1.分析如下程序的输出结果
int a[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = a + 5; printf("%d\n",p[2]);//8
- 分析如下程序的输出结果
#include <stdio.h> int main() { int a[10] = {0,1,2,3,4,5,6,7,8,9}; // printf("%ld\n",sizeof(a));//sizeof(a) a此时代表整个数组 // //sizeof(a) 求整个数组所占的字节数 // printf("%p\n",a);//此时a当做指针来用, a ==> &a[0] // printf("%p\n",&a[0]); // printf("%d\n",*a);// a[0] printf("%d\n",*(a + 1));// //*(&a[0] + 1) ==> *(&a[1]) ==> a[1] return 0; }
10 多维数组与指针
预备知识 :
数组名可以当指针用, a , 当指针用 ,&a[0]
C语言中,只有一维数组, 二维/三维/...其实都是一维数组
int a[3][4]; 数组a有三个元素 : a[0] ___ ___ ___ ___ a[1] ___ ___ ___ ___ a[2] ___ ___ ___ ___ a[0] a[1] a[2] 又是一个数组 *( (a + 1 ) + 1 ) 此处数组名a ,当指针用 *( (a + 1 ) + 1 ) ==> *( (&a[0] + 1) + 1) ==> *(&a[1] + 1) ==> *(&a[2]) ==> a[2] *( *(a + 1) + 2) ==> *( *(&a[0] + 1) + 2) ==> *(*&a[1] + 2) ==> *( a[1] + 2) ==> *(&a[1][0] + 2) ==> *(&a[1][2]) ==> a[1][2] *(a[1] + 2 ) ===> a[1][2] --------------- a[0] + 1 此处数组名a[0] ,当指针用 a[0] + 1 ==> &a[0][0] + 1 ==> &a[0][1] --------------- a + 1 此处数组名a 当指针用 a + 1 ==> &a[0] + 1 ==> &a[1] &a[1] 与 &a[1][0] 仅数值相同, 含义和类型完全不一样的 typeof(&a[1]) : typeof(a[1]) * ==> int[4] * typeof(&a[1][0]) : typeof(a[1][0]) * ==> int* --------------- a (1) 代表整个数组 sizeof(a),typeof(a) , &a (2) 当指针用 ,代表首元素的地址, &a[0] a[0] (1) 代表整个数组 (2) 当指针用,代表首元素的地址,&a[0][0] &a[0][0] 代表第0行第0列的那个元素的地址 typeof(&a[0][0]) : typeof(a[0][0]) * ==> int* &a[0] 代表第0行个那个数组的地址 typeof(&a[0]) : typeof(a[0]) * ==> int[4]* &a 代表的整个数组a的地址 typeof(&a) : typeof(a) * ==> int[4][3] *int b[4] ;//b 是一个含有4个int类型的元素的数组 //typeof(b) : int[4] // 元素类型 [元素个数] int a[3][4];//a是一个含有3个元素, 且每个元素又是4个int类型的数组 的数组. typeof(a) : int[4] [3] //元素类型 [元素个数]
int a[4]; //定义一个指针p,指向数组a ___int (*p)[4]___;定义p int[4] *p p = &a;
11 数组指针 与 指针数组
11.1 数组指针
数组指针是什么? B
A 数组 B 指针
数组指针就是一个指针, 指向一个数组的指针.
函数指针就是一个指针,指向一个函数的指针.
数组指针是一个指向数组的指针
int a[4]; //定义一个指针p, 指向数组a typeof(a) * p;//定义p typeof(a) * p; ==> int[4] *p;==> int (*p)[4] int (*p)[4];//int[4] *p p = &a;
11.2 指针数组
指针数组是一个数组,里面的元素是指针类型
如 :
int* p[4];//p是一个数组,里面有4个元素,元素的类型是int* 数组定义格式: 元素类型 数组名[元素个数] int* p [4] int (*p)[4];//p是一个指针,指向一个数组
作业 :
-
分析如下程序的输出结果
int a[4] = {1,2,3,4}; int* p[4]; for(int i = 0;i < 4;i++) { p[i] = a + i; } for(int i = 0;i < 4;i++) { printf("%d ",*p[i]); } printf("\n");//1 2 3 4![image-20230712092954392]()
-
分析如下程序的输出结果
int a[3][4] = { 1,2,3,4, 5,6,7,8, 9,10,11,12 }; int (*p)[4]; p = a; printf("%d\n",p[1][2]);//7 -
分析如下程序的输出结果
(1) int a[12] = {1,2,3,4,5,6,7,8,9,10,11,12}; printf("%p %p %p\n",a,a + 1,&a + 1); (2) int b[3][4] = { 1,2,3,4, 5,6,7,8, 9,10,11,12 }; printf("%p %p %p %p\n",b,b + 1,&b + 1,b[1] + 1); (3) int a[5] = {1,2,3,4,5}; int *ptr; ptr = (int*)(&a + 1); printf("%d\n",*(ptr - 1));//5 (4) int a[5] = {1,2,3,4,5}; int *ptr; ptr = (int*)&a + 1; printf("%d\n",*(ptr - 1));//1![image-20230712095023046]()
![image-20230712095509624]()
有一个问题, 需要各位高手来解决一下 :
int main() { int a = 250,b = 360; swap(...);//调用swap函数 printf("a = %d,b = %d\n",a,b);//a = 360,b = 250 } //设计一个函数swap 来交换 main 中 a 和 b的值 ... swap(...) { }我有一个设想 :
int main() { int a = 250,b = 360; swap(...);//调用swap函数 printf("a = %d,b = %d\n",a,b);//a = 360,b = 250 } //设计一个函数swap 来交换 main 中 a 和 b的值 void swap(int a,int b) { int t; t = a; a = b; b = t; }关于函数调用 :
(1) 实参和形参占用不同的存储空间;
(2) 在调用时,是把实参的值 赋值 给形参;
"值传递" : 相应的形参 = 实参的值
12 指针作函数的参数
函数的形式参数是一个指针类型.
void func(int *p) { *p = 251; } int main() { int a = 250; func(a);//有问题 //p = a; //typeof(a) : int //typeof(p) : int * //类型不匹配 func(&a);//可以的 //typeof(&a) : typeof(a) * ==> int* a 的值 就变成了 251 return 0; }通过传递指针,来修改(或访问)函数外部变量或对象
练习 :
重新设计一个swap 函数, 使得他可以交换两个外部变量的值.
#include <stdio.h> void swap(int a,int b); //设计一个函数swap 来交换 main 中 a 和 b的值 void swap_v2(int* p,int* q) { int t; t = *p; *p = *q; *q = t; } int main() { int a = 250,b = 360; //swap(a,b);//调用swap函数 swap_v2(&a,&b);//调用swap函数 printf("a = %d,b = %d\n",a,b);//a = 360,b = 250 } //设计一个函数swap 来交换 main 中 a 和 b的值 void swap(int a,int b) { int t; t = a; a = b; b = t; }
13 数组作为函数的参数
数组作为函数的参数 :
数组元素的类型 数组名[],数组元素的个数int a[10]; #include <stdio.h> /* find_max : 找一个一维数组的最大值,只求一维数组 int类型元素的数组的最大值 @a : 元素类型为int 的数组名 @n : 元素的个数,求 n 个元素中的最大值 返回值 : 返回最大值 */ int find_max(int b[],int n) { //b[0] b[1] ... b[n-1] int max = b[0]; for(int i = 1;i < n;i++) { if(b[i] > max) { max = b[i]; } } return max; } int main() { int a[10] = {1,2,3,4,250,6,7,8,9,10}; int max; max = find_max(a,10); printf("%d\n",find_max(a,10)); printf("%d\n",max); return 0; }分析一下 :
int find_max(int b[],int n); 在接收实际参数 数组a时 : b = a;//此处数组名a 当指针用 ==> b = &a[0];//typeof(&a[0]) : typeof(a[0]) * ==> int * 函数的形式参数 : int b[] ==> int *b //b就是一个指针 int find_max(int *b,int n);结论 :
数组名当函数参数时,就是当指针用
练习 :
1.写一个函数,求一个二维数组的最大值
//求二位数组的最大值 //数组作函数的参数时 : // 元素的类型 数组名[],元素个数 // int[5] b[] int n //==> int b[][5] ,int n int find_max(int b[][5],int n) { int max = b[0][0]; int i,j; for(i = 0;i < n;i++) { for(j = 0;j < 5;j++) { if(b[i][j] > max) { max = b[i][j]; } } } return max; } int main() { int a[3][5] = { 1,2,3,4,5, 6,7,8,9,10, 11,12,13,14,15 }; printf("%d\n",find_max(a,3)); return 0; }int find_max(int b[][5],int n); find_max(a,3) b = a 两个数组名,此处是当指针用 b = &a[0] typeof(&a[0]) : typeof(a[0]) * ==> int[5] * b 的类型就是 : int[5] * b ==> int (*b)[5]思考 :
int a[3][5] = { 1,2,3,4,5, 6,7,8,9,10, 11,12,13,14,15 }; 和 int a[15] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}; 从底层存储器的角度分析, 上面两种定义方式有什么区别?#include <stdio.h> //求二位数组的最大值 //数组作函数的参数时 : // 元素的类型 数组名[],元素个数 // int[5] b[] int n //==> int b[][5] ,int n //int find_max(int b[][5],int n) int find_max(int (*b)[5],int n) { int max = b[0][0]; int i,j; for(i = 0;i < n;i++) { for(j = 0;j < 5;j++) { if(b[i][j] > max) { max = b[i][j]; } } } return max; } int find_max_v2(int *b,int n )//int find_max_v2(int b[],int n ) { int max = b[0]; for(int i = 0;i < n;i++) { if(b[i] > max) { max = b[i]; } } return max; } int main() { int a[3][5] = { 1,2,3,4,520, 6,7,8,999,10, 11,12,13,14,15 }; //printf("%d\n",find_max(a,3)); printf("%d\n",find_max_v2(&a[0][0],15)); return 0; }
回顾一下 : 有地址的对象,我们可以定义一个指针变量,来保存他们的地址. 如 : int a = 5; int *pa = &a; int b[4]; int (*pb)[4];//pb数组指针 指向 int[4] pb = &b; ... 函数也有地址,我们也可以定义一个指针变量,来保存函数的地址,这类指针 称之为 : 函数指针
14 函数指针
14.1 什么是函数指针?
函数指针就是一个指针, 这个指针指向了一个函数(保存了一个函数的地址).
14.2 函数指针如何定义呢?
指针变量的定义格式为 :
指向的类型 *指针变量名;函数的类型在C语言中如何描述呢?
int sum(int a,int b) { } //sum 是一个带两个 int 类型的参数 且返回值为int类型的函数. void abc(float a) { } //abc是一个带 一个float类型参数且无返回值的函数 int find_max(int *b,int n) { } //find_max是一个第一个参数为int *,第二个参数为 int ,返回值为 int的函数 //...描述函数类型 :
返回值类型 (参数类型列表)typeof(sum) : int (int,int) typeof(abc) : void (float) typeof(find_max) : int (int *,int)定义一个函数指针变量p,保存函数sum的地址,如何定义呢?
typeof(sum) *p; ==> int (int,int) *p; ==> int (*p)(int,int); //定义了一个函数指针变量p //p指向一个函数,指向的类型为 : int (int,int)在C语言中,定义函数指针变量的格式如下 :
指向函数的返回值类型 (*函数指针变量名)(指向函数的参数的类型列表);
14.3 如何获取一个函数的地址呢?
& 取地址符
如 :
&函数名 --> 某某函数的地址 &sum &find_max同时,在C语言中,函数名本身就是一个地址,函数名代表的是函数的首地址.
&sum <==> sum &find_max <==> find_max ... p = &sum <==> p = sum
14.4 如何通过函数指针去调用函数呢?
p = &find_max; *p = *&find_max ==> find_max通过函数指针去调用他指向的函数的语法如下 :
- (1) (*函数指针变量)(实参表达式列表)
- (2) 函数指针变量(实参表达式列表)
练习 :
1.写一个函数,返回一个整型数组的最大元素的地址,并且通过函数指针去调用这个函数.
(1) 明确任务目标 "求数组最大值元素的那个地址" find_max_addr (2) 确定输入参数 "输入一个数组" : 数组的首地址 和 元素个数 (3) 确定返回值 返回 数组中最大值元素的地址 int* /* find_max_addr : 返回一个整型数组最大值元素的地址 @a : 指针, 数组的首地址 @n : 数组元素个数 返回值 : 返回 数组中最大值元素的地址 */ int* find_max_addr(int *a,int n) { //int max = a[0];//max保存最大值 int *p = &a[0]; for(int i = 0;i < n;i++) { if(a[i] > *p ) { //max = a[i]; p = &a[i]; } } return p; } int main() { int a[5] = {1,2,5,4,3}; int* (*p)(int *,int); p = find_max_addr; printf("%p\n",p(a,5)); printf("%p\n",&a[2]); return 0; }
14.5 函数指针有什么用?
函数指针是通过 指针 去调用他指向的函数.
为什么不直接用函数名去调用呢?
直接用函数名调用不是更简单!!!
那么到底为什么要用函数指针呢?
call me back
callback 回调
函数指针主要是为了实现回调 : 现在不调用, 回过头调用.
思考 :
int *p(int,int); int (*p)(int ,int); 上面两个p是一样的吗? 不一样的 第一个p : 函数声明 , p是一个函数名,返回值为 int* 第二个p : 函数指针变量p的定义, p是一个函数指针变量名,指向的函数 返回值为int,带两个int的
15 二级指针与多级指针(就是一个指针)
int a = 5; //定义一个指针变量p ,来保存a的地址 int *p = &a; //可以定义一个指针变量p2,来保存p的地址 int* *p2 = &p; //可以定义一个指针变量 p3,来保存p2的地址 int** *p3 = &p2; ... //*a 不对的, *指针运算符, 他后面需要接一个地址 *p //p的右值, &a *p ==> *&a ==> a //**p **p ==> **&a ==> *a 不可以的 **p2 //p2 的右值 &p **p2 ==> **&p ==> *p ==> *&a ==> a ***p3 //p3的右值 &p2 ***p3 ==> ***&p2 ==> **p2 ==>**&p ==> *p ==> *&a ==> a **p3 //p3的右值 &p2 **p3 ==> **&p2 ==> *p2 ==> *&p ==> p ****p3 ==> ... ==> *a 把a的值当作一个地址 a的值不一定是一个地址,有问题 *a *后面要接一个地址.不可以#include <stdio.h> int main() { int a = 5; int b = 250; int *p = &a; int** p2 = &p; int*** p3 = &p2; *p2 = &b; printf("%p\n",*p2);//*p2 ==> *&p ==>p printf("%d\n",**p2); printf("%p\n",&a); return 0; }注意
if x 是一个指针 , x 和 *x 分别代表什么?
x 代表本身
*x 代表x指向的对象.
接下来 ,我们在看下面这段代码
int n; scanf("%d",&n);//输入数组元素的个数 int a[n];//这个在标准C里面是不允许的,定义数组时,元素个数一定要确定好, //C语言只支持 "静态数组" //有没有办法实现 "动态数组"? //动态数组 : 在程序运行期间,根据用户输入的值,来确定数组的大小.
16 动态内存分配的函数
"动态内存" : 在程序运行期间, 动态分配内存空间, 一般是在 "堆,heap"空间上分配
malloc/realloc/calloc/free
malloc : memory allocate 内存分配
realloc : repeat allocate 再分配
重新分配 : 一次内存分配完成后,后面用的时候,发现不够用,再分配.
calloc : clear allocate 分配内存时同时清 0
free : 释放
动态分配的内存,需要在你不用的时候,要手动释放free, 否则,这个空间一直是你的,
动态内存分配(malloc/realloc/calloc) 如果分配了空间,一直存在,直到你手动free或进程结束.
有时候,把动态分配的内存"匿名内存" , 匿名内存只能通过他的指针去访问.
16.1 malloc 内存分配
#include <stdlib.h> malloc用来向系统申请size字节大小的内存(连续的), 并且把这段内存的首地址返回(void*) void *malloc(size_t size); size : 你要分配多大的空间,单位是字节,你要多少字节的空间 返回值 : 成功 返回分配的空间的首地址 void* 失败 返回NULL //void* 表示这个东西是一个指针(地址),至于是什么类型的对象,他不关心.#include <stdio.h> #include <stdlib.h> int main() { int *p; //malloc 是一个函数,这个函数是系统写的,不是我们写的,一定要包含头文件 //这个函数是 用来 从 堆空间中 分配内存. //p = (int*)malloc(sizeof(int)); p = (int*)malloc(4); *p = 360;//p指向一个 堆空间对象 //*p 就是表示p 指向的对象 printf("%d\n",*p); return 0; }练习 :
用malloc实现动态数组.
int n; scnaf("%d",&n); int a[n]; //不可以的int *a = malloc(); for(int i = 0;i < n;i++) { scanf("%d",...) } for(int i = 0;i < n;i++) { printf("%d ",*()); printf("%d ",a[i]); }#include <stdio.h> #include <stdlib.h> int main() { //int n; //scnaf("%d",&n); //int a[n]; //不可以的 int n; scanf("%d",&n); int *a = (int*)malloc(sizeof(int)*n); for(int i = 0;i < n;i++) { scanf("%d",a + i); } for(int i = 0;i < n;i++) { //printf("%d ",*(a + i)); printf("%d ",a[i]); } printf("\n"); return 0; }
16.2 calloc 分配内存时同时清 0
#include <stdlib.h> calloc 也是用来从"堆空间" 分配内存的,只不过calloc分配的内存会自动清0 void *calloc(size_t n, size_t size); n : 要分配多少个元素 size : 每个元素占多少个字节 calloc 分配的总字节大小为 : n*size 返回值 : 成功 返回分配到的空间的首地址 失败 返回NULL#include <stdio.h> #include <stdlib.h> int main() { //int n; //scnaf("%d",&n); //int a[n]; //不可以的 int n; scanf("%d",&n); int *a = (int*)calloc(n,sizeof(int)); for(int i = 0;i < n;i++) { //printf("%d ",*(a + i)); printf("%d ",a[i]); } printf("\n"); return 0; }
16.3 realloc 再分配
#include <stdlib.h> realloc 用来把ptr(malloc/calloc/realloc返回的地址,或者是NULL) 指向的空间,扩大到size大小 void *realloc(void *ptr, size_t size); 1. ptr == NULL,首次分配 realloc(NULL,size) <==> malloc(size) ptr != NULL <1> size > 原来的大小 "扩建" realloc用来把ptr指向的内存,扩大到size大小 原来的内存内容保持不变,后面新增的内存不会初始化. 他只负责分配空间. (1)原址扩建 (2)整体搬迁 2. size == 0 realloc(ptr,0) <==> free(ptr) size < 原来的大小 这种情况,作者没有考虑到这种行为,这种行为是未定义的(什么结果都有可能发生)
16.4 free 释放
#include <stdlib.h> free用来释放动态分配的空间的. void free(void *ptr); ptr : 要释放的空间的首地址,这个地址要是 malloc/calloc/realloc的返回值,其他地址 返回值 : 无
思考 :
- C语言中"内存泄漏/垃圾内存"
什么是内存泄漏?
内存泄漏有什么危害?
内存泄漏是如何产生的?
如何避免内存泄漏?
17 字符串 和 字符串函数
17.1 字符串
字符串 : 一串字符, "0个或多个字符"
单个字符, C语言中有专门的类型
char/unsigned char --> 保存的是单个字符的ASCII码(8bits的整数)
C语言中,没有字符串的类型.
C语言中的 字符串 是通过char*(字符型指针) 来实现的.
C语言的字符串是 一串字符(0个或多个) , 这些字符在内存地址上是连续的.
如果我们知道"字符串" 的首地址(第一个字符的地址) : 0x3000
推出字符串的第二个字符的地址 : 0x3001
推出字符串的第三个字符的地址 : 0x3002
...
并且我们知道每个字符的地址,就可以通过他的地址(char*)来访问每个字符.
通过字符串的首地址,不停的 +1,就可以得到每个字符的地址,
但是你通过字符的首地址,
能不能知道这个字符串有多少个字符呢?
不能知道
所以,C语言规定字符串的末尾加一个 "字符串结束的标志"
null字符,空字符,0,'\0'char* 用来保存字符串的首地址的,并且约定好字符串结束的标志'\0'.
你就可以通过char* 来访问或操作字符串.
char s[10]; //数组名 s,当指针, &s[0], char* scanf("%s",s);//%s从键盘上输入一串字符,输入到一个char*的地址中去 scanf("%s",&s[0]);//一样的 scanf("%s",&s);//不行的 类型不匹配%s --> char* //typeof(&s) ==> typeof(s)* ==> char[10]* printf("%s\n",s);//%s输出一个字符串,后面也需要接一个字符串类型(char*)
17.2 字符串常量
字符串常量四不可以被修改的字符串,只能读.
字符串常量保存在一个.rodata的内存区.
如 :
C代码中,所有 " " 引起来的都是字符串常量 并且 所有 用 " "引起的表达式的值, 就是这个字符串的首地址. typeof("abc123") : const char * //const 常量修饰词 char* p = "acb123";//这是可以的 //表达式 "abc123"的值是字符'a'的地址 &'a' p + 1 ==> c *(p + 1 ) = 'A';//不可以的 ""引起来的是常量,常量不能被改变 "aAb123" --------- 常量内存
ro read only 只读区域
存储哪些只能读的数据,常量 ,如 : "abcd"
rw read&write 可读可写区域
存储那些变量
如 : 没有加const 修饰的变量 ,数组,指针变量
int a;//a存储在可读可写区域 int b[100];//b存储在可读可写区域 char c[100];//c存储在可读可写区域 int *p;//p存储在可读可写区域 char ch;//ch存储在可读可写区域
17.3 字符串变量
把字符串(多个字符) 存储在一个可读可写的区域就可以了.
char s[100];//s存储在可读可写区域
你把字符串存储在s这个数组中,字符串变量.
char *p = (char*)malloc(100);//malloc 分配的空间也是 可读可写的 你把字符串存储到p指向的空间, 字符串变量如 :
char s[3]; s[0] = 'a'; s[1] = 'b'; s[2] = '\0'; ===> char s[3] = {"ab"}; s[0] = 'a'; s[1] = 'b'; s[2] = '\0'; s[1] = 'B';//可以的 char s[3] = {"aB"}练习 1 :
1.分析如下情况
char s1[] = {'a','b','c'}; char s2[] = {"abc"}; 请问数组 s1的元素个数是几个? 3 s2的元素个数是几个? 4 sizeof(s1) = ? 3 sizeof(s2) = ? 4 字符串s1中字符个数多少? 3 字符串s2中字符个数多少? 3 printf("%s\n",s1); //abc???? printf("%s\n",s2); //abc2.分析如下情况
char s3[5] = {'a','b','c'}; char s4[5] = {"abcd"}; sizeof(s3) = ? 5 sizeof(s4) = ? 5 printf("%s\n",s3); abc printf("%s\n",s4); abcd3.分析如下程序的输出结果
char s[10] = {"abcde"}; s += 2;//error s = s + 2 //s是一个数组名 常量 s[2] = 'A';//可以的 数组s是可读可写 *(s + 2) = 'A';//*(&s[0] + 2) ==> s[2] printf("%s\n",s);//abAde abAdA4.分析如下程序的输出结果
char *s = "abcde"; s += 2;//可以的 s是一个指针变量 *(s + 2) = 'A';//不可以的 s指针的空间 只读空间 printf("%s\n",s);#include <stdio.h> int main() { //printf("%s\n","abcd123");//可以 "abc123" 类型是char *,值为 首字符的地址. char *p = "abcd123"; //printf("%s\n",p);//可以的 //printf("%s\n",p + 2); *(p + 2) = 'A';//error printf("%c\n",*(p + 2)); return 0; }
17.4 几个常用的字符串处理函数
(1) strlen 用来求一个字符串的长度
NAME strlen - calculate the length of a string SYNOPSIS #include <string.h> strlen 求一个字符串的长度 size_t strlen(const char *s); @s : s指向的字符串,遇到'\0'结束,求的长度不包含'\0' 返回值 : 返回字符串的长度例子 :
int l; l = strlen("abcde"); l = 5 char s[10] = {"abc"}; l = strlen(s); l = 3 strlen : 是一个函数,用来计算一个字符串的长度的,算到第一个 '\0' 为止. sizeof : 是一个运算符,用来求一个对象(或类型)自己本身所占内存的字节数 char s1[4] = {'1','0'}; sizeof(s1) = ? 4 l = strlen(s1); l = ? 2 char s2[4] = {'1',0,'3'}; sizeof(s2) = ? 4 l = strlen(s2); l = ? 1 l = strlen("abcd\123456\0ef"); l = ?8 //\123算一个字符
(2) strcpy 字符串拷贝函数
cpy : copy
NAME strcpy, strncpy - copy a string SYNOPSIS #include <string.h> strcpy用来把src指向的字符串拷贝到dest指向的空间中去, 拷贝到 \0 为止(\0也会拷贝) char *strcpy(char *dest, const char *src); dest : 指向 "目的地",dest指向的空间,要保证可写,空间要足够大 src : 指向"源字符串",把src指向的字符串拷贝到dest指向的空间中 返回值 : 成功 返回拷贝后字符串的首地址. strcpy有一个巨大的bug : 他没有考虑到越界的问题, 有可能会导致内存的非法访问 strncpy 就是为了解决strcpy的bug而产生的. strncpy用来把src指向的字符串拷贝到dest指向的空间中去, 但是strncpy顶多拷贝n个字符(一般来说,n为dest指向空间的最大容量) 那么strncpy到底拷贝了多少个字符呢? <= n (1) 遇到 \0 拷贝结束(此时后面顶多拷贝 n - strlen(src)个 \0) (2) 一直没有遇到\0,但是已经拷贝了n 个字符,拷贝结束(此时\0不会拷贝) char *strncpy(char *dest, const char *src, size_t n); dest : 指向目标空间 src : 指向源字符串 n : 规定拷贝 n 个字节,一般来说,n为dest指向空间的最大容量 返回值 : 成功 返回拷贝后字符串的首地址(dest)#include <stdio.h> #include <string.h> int main() { char s[10]; strcpy(s,"abcde"); printf("%s\n",s); return 0; }#include <stdio.h> #include <string.h> int main() { // char s[4]; // strcpy(s,"abcde"); //有问题的 char s[4]; strncpy(s,"abcde",3); printf("%s\n",s); return 0; }练习 :
写一个函数, 实现strncpy的功能.
char* my_stncpy(char* dest,char* src,size_t n) { for(int i = 0;i < n;i++) { *dest = *src; dest++; src++; } *dest = '\0'; return dest; } char* strncpy(char *dest, const char *src, size_t n) { size_t i; for (i = 0; i < n && src[i] != '\0'; i++) { dest[i] = src[i]; } for ( ; i < n; i++) { dest[i] = '\0'; } return dest; }#include <stdio.h> #include <string.h> int main() { char s1[8]; char s2[8] = {"abcde"}; printf("%p\n%p\n",s1,s2); //strcpy(s1,"123456789"); //strncpy(s1,"123456789",8); strncpy(s1,"ABCDEF\0ab",9); printf("%s\n",s1); printf("%s\n",s2); return 0; }
(3) mencpy 内存拷贝
NAME memcpy - copy memory area SYNOPSIS #include <string.h> memcpy 用来把src指向的内存中的前面n个字节,拷贝到dest指向的空间中去. void *memcpy(void *dest, const void *src, size_t n); dest : 指向目标内存块,要保证dest指向的空间足够大 src : 指向源内存块 n : 要拷贝的字节数 返回值 : 返回 dest的这个地址思考 :
strncpy 和 memcpy 有什么区别?
strncpy是字符串的拷贝函数,顶多拷贝n个字节(遇到\0拷贝结束,多补几个 n - i个\0)
memcpy是内存块拷贝函数(没有类型概念) ,成功的话, 一个会拷贝n个字节.
so ,
字符串拷贝,最好用 strncpy
其他内存拷贝,用memcpy
例子 :
char s1[8]; char s2[8]; scanf("%s",s2); //从键盘上输入一个字符串(遇到\n),存放到s2指向的空间中去 //最后加一个\0到s2这个空间 //用户输入的字符串长度不定 //要把s2指向的字符串, 拷贝到s1 memcpy(s1,s2,strlen(s2) + 1); //"+1" 我想把s2这个字符串后面\0也拷贝 //strlen(s2) + 1 <= 8 没有问题 //strlen(s2) + 1 > 8 就有问题了,越界了 strncpy(s1,s2,8); //strlen(s2) < 8 可以完整的拷贝字符串到s1 //strlen(s2) >= 9 也不会越界,因为顶多拷贝8个
(4) bzero 把一个字符串清0
AME bzero - zero a byte string SYNOPSIS #include <strings.h> bzero用来把s指向的空间的前面n个字节清0 void bzero(void *s, size_t n);
(5) strcat/strncat 连接两个字符串
NAME strcat, strncat - concatenate two strings SYNOPSIS #include <string.h> strcat 用来把src指向的字符串,连接到dest指向的字符串的末尾去."尾部连接" strcat先找到dest指向的那个字符串的末尾(\0),从\0处开始接src指向 的字符串,最后加一个\0 char *strcat(char *dest, const char *src); dest : 指向目标字符串,其空间要足够大 why? 如果不足够大,就有越界的风险. src : 指向源字符串 返回值: 返回 连接后的字符串的首地址(dest) strcat也有一个bug,没有考虑到dest指向的空间,是否足够大的问题,有越界的风险 strncat 就是用来修复strcat这个bug的 strcat 用来把src指向的字符串,连接到dest指向的字符串的末尾去. 但是他顶多拷贝n个字符 (1) 遇到\0拷贝结束 <= n (2) 即使没有遇到\0,但是已经拷贝了n个字符啦,也结束(此时\0不会拷贝) char *strncat(char *dest, const char *src, size_t n); A simple implementation of strncat() might be: char * strncat(char *dest, const char *src, size_t n) { size_t dest_len = strlen(dest); size_t i; for (i = 0 ; i < n && src[i] != '\0' ; i++) dest[dest_len + i] = src[i]; dest[dest_len + i] = '\0'; return dest; }
(6) strcmp/strncmp 比较两个字符串
str : string 字符串
cmp : compare 比较
strcmp 是字符串比较函数
两个字符串如何比较呢?
一个一个字符PK,PK他们的ASCII码
while(第一个字符没有结束 || 第二个字符串没有结束) { if 第一个字符串的第i个字符 < 第二个字符串的第i个字符 return -1;//表示第一个字符串 小 else if 第一个字符串的第i个字符 > 第二个字符串的第i个字符 return 1;//表示第一个字符串 大 else //第一个字符串的第i个字符 == 第二个字符串的第i个字符 i++;//比较下一个 } return 0;//表示两个字符串相等,一模一样NAME strcmp, strncmp - compare two strings SYNOPSIS #include <string.h> int strcmp(const char *s1, const char *s2); strncmp 只比较前面的n个字符 int strncmp(const char *s1, const char *s2, size_t n);例子 :
int m = strcmp("123","ABC") m < 0 m = strcmp("123","123\0abc") m == 0 m = strcmp("1234","123") m > 0 m = strncmp("1234","123",3); m == 0






浙公网安备 33010602011771号