目录
• 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
注:对于“[ ]”来说,只是一个操作符,比如arr[i]即*(arr+i)
• main函数中将arr作为参数传到函数,传的只是arr的首元素地址。
一. 指针初步
1.概念定义
地址:我们在内存中开辟空间时,为了方便后续访问,每个数据有确切的地址。
指针:指向数据的地址,并将其地址储存在指针变量中。
2.基本运算符
• 取地址操作符(&)
%p是用于打印地址的格式。
• 解引⽤操作符 (*)
*pa解引用a的数据。
3.指针变量的⼤⼩
#include <stdio.h> //指针变量的⼤⼩取决于地址的⼤⼩ //32位平台下地址是32个bit位(即4个字节) //64位平台下地址是64个bit位(即8个字节) int main() { printf("%zd\n", sizeof(char *)); printf("%zd\n", sizeof(short *)); printf("%zd\n", sizeof(int *)); printf("%zd\n", sizeof(double *)); return 0; }
• 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节。 • 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节。 • 指针变量的⼤⼩与类型⽆关,只要是指针类型的变量,在相同的平台下,⼤⼩都是相同。
二. 指针运算
1.指针+- 整数
数组在内存中是连续存放的,随着数组下标的增长,地址由高到低变化。
#include <stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &arr[0]; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); for(i=0; i<sz; i++) { printf("%d ", *(p+i)); } return 0; }
注:p+1,即指针(地址增加一个int型字节大小[元素])。
2.指针 - 指针
指针 - 指针得到的是指针之间相差的元素个数。
#include <stdio.h> int my_strlen(char *s) { char *p = s; while(*p != '\0' ) p++; return p-s; } int main() { printf("%d\n", my_strlen("abc")); return 0; }
3.指针的关系运算
#include <stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &arr[0]; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); while(p<arr+sz) //指针的⼤⼩⽐较 { printf("%d ", *p); p++; } return 0; }
通过指针访问数组内元素,while循环遍历数组每一个数据。
三. 野指针
1.野指针成因
• 指针未初始化
#include <stdio.h> int main() { int *p;//局部变量指针未初始化,默认为随机值 *p = 20; return 0; }
• 指针越界访问
#include <stdio.h> int main() { int arr[10] = {0}; int *p = &arr[0]; int i = 0; for(i=0; i<=11; i++) { //当指针指向的范围超出数组arr的范围时,p就是野指针 *(p++) = i; } return 0; }
• 指针指向的空间释放
#include <stdio.h> int* test() { int n = 100; return &n; } int main() { int*p = test(); printf("%d\n", *p); return 0; }
局部变量n作用域在test函数,当程序运行至主函数时,n的空间被内存收回。
2.如何规避野指针
• 指针初始化
如果明确知道指针指向哪⾥就直接赋值地址,但不确定指针应该指向哪⾥,可以给指针赋值NULL。NULL 是C语⾔中定义的⼀个标识符常量,值是0(0也是地址),这个地址是⽆法使⽤的,读写时会报错。
#include <stdio.h> int main() { int num = 10; int*p1 = # int*p2 = NULL; return 0; }
• 小心指针越界
程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
• 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
int main() { int arr[10] = {1,2,3,4,5,67,7,8,9,10}; int *p = &arr[0]; for(i=0; i<10; i++) { *(p++) = i; } //此时p已经越界了,可以把p置为NULL p = NULL; //下次使⽤的时候,判断p不为NULL的时候再使⽤ //... p = &arr[0];//重新让p获得地址 if(p != NULL) //判断 { //... } return 0; }
• 避免返回局部变量的地址
如造成野指针的第3个例⼦,不要返回局部变量的地址。
四. assert断言
1.assert基本概念
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。
上述代码在程序运⾏到这⼀⾏语句时,检查变量 p 是否等于 NULL 。若不等于 NULL ,程序继续运⾏,否则终⽌运⾏,并且显示报错信息提⽰。 assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零, assert() 就会报错,在标准错误流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
2.assert启动关闭
assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。 然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移 除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语 句。 assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。 ⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样成开 发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响⽤⼾使⽤时程序的效率。
开启>>
#define NDEBUG # include <assert.h> int main() { int var= 8; assert (var==1); system("pause"); return 0; }
关闭>>
#define NDEBUG # include <assert.h> int main() { int var= 8; assert (var==1); system("pause"); return 0; }
五. 指针的传值调用与传址调用
例如:写⼀个函数,交换两个整型变量的值,分别采用传值调用与传址调用这两种方法
1.传值调用
#include <stdio.h> void Swap1(int x, int y) { int tmp = x; x = y; y = tmp; } int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap1(a, b); printf("交换后:a=%d b=%d\n", a, b); return 0; }
显然,在这里,传值调用并没有将a,b的值进行交换。
2.传址调用
#include <stdio.h> void Swap2(int*px, int*py) { int tmp = 0; tmp = *px; *px = *py; *py = tmp; } int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap2(&a, &b); printf("交换后:a=%d b=%d\n", a, b); return 0; }
实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。顾名思义, 形参是实参的一份临时拷贝。
如果要发生交换的效果,形参就应该接收main函数中的实参的地址所以……
因此我们可以在main函数中将a和b的地址(通过指针变量)传递给Swap函数,Swap 函数⾥边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。
六. 数组 + 指针(深入解析)
1. 数组名的理解
int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &arr[0];
sizeof(数组名):表示整个数组,计算的是整个数组的大小,单位是字节。
&数组名:也表示整个数组,取出的是整个数组的地址。
#include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("&arr[0] = %p\n", &arr[0]); printf("arr = %p\n", arr); return 0; }
&arr[0]即首元素地址,arr(数组名),在debug x86环境下,两地址相同。
注:对于“[ ]”来说,只是一个操作符,比如arr[i]即*(arr+i)
因此,可以换成*(i+arr),加法的交换律。
• arr[ i ] == *(p+i)== *(arr)+i
• main函数中将arr作为参数传到函数,传的只是arr的首元素地址。
2. 使⽤指针访问数组
#include <stdio.h> int main() { int arr[10] = {0}; //输⼊ int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); //输⼊ int* p = arr; for(i=0; i<sz; i++) { scanf("%d", p+i); //scanf("%d", arr+i);//也可以这样写 } //输出 for(i=0; i<sz; i++) { printf("%d ", *(p+i)); } return 0; }
观上,数组名(arr)作为数组首元素地址,在这里,我们可以直接将arr赋值给p(指针变量),我们可以通过数组下标访问数组的元素,也可以通过指针变量解引用“*(p+i)”进行访问。
3. ⼀维数组传参的本质
#include<stdiio.h> void test(int arr[]) { int sz2 = sizeof(arr)/sizeof(arr[0]); printf("sz2 = %d\n", sz2); } int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int sz1 = sizeof(arr)/sizeof(arr[0]); printf("sz1 = %d\n", sz1); test(arr); return 0; }
从结果来看,sz1得到了arr数组中元素个数,然而sz2却打印错误,究其原因,也就是说本质上数组传参传递的是数组⾸元素的地址,test函数中形参得到的应该是数组第一个元素。
tips: ⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
4. 冒泡排序
• 代码实现
void bubble_sort(int arr[], int sz)//参数接收数组元素个数 { int i = 0; for(i=0; i<sz-1; i++) { int flag = 1;//假设这⼀趟已经有序了 int j = 0; for(j=0; j<sz-i-1; j++) { if(arr[j] > arr[j+1]) { flag = 0;//发⽣交换就说明,⽆序 int tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; } } if(flag == 1)//这⼀趟没交换就说明已经有序,后续⽆序排序了 break; } } int main() { int arr[] = {3,1,7,5,8,9,0,2,4,6}; int sz = sizeof(arr)/sizeof(arr[0]); bubble_sort(arr, sz); int i = 0; for(i=0; i<sz; i++) { printf("%d ", arr[i]); } return 0; }
如上,对10个数字进行冒泡排序,第一个“3”,与1,7,5,8……进行比较,采取相邻两位进行比较法,循环往复。加入flag进行优化,提高程序运行效率。
5. ⼆级指针
指针变量也是变量,是变量就有地址,二级指针就用于存放指针变量的地址。 *ppa 通过对ppa中的地址进⾏解引⽤,这样找到的是 pa , *ppa 其实访问的就是 pa .
int b = 20; *ppa = &b;//等价于 pa = &b;
**ppa 先通过 *ppa 找到 pa ,然后对 pa 进⾏解引⽤操作: *pa ,那找到的是 a .
**ppa = 30; //等价于*pa = 30; //等价于a = 30;
6. 指针数组
指针数组是指针还是数组? 我们类⽐⼀下,整型数组,是存放整型的数组,字符数组是存放字符的数组。
7. 指针数组模拟⼆维数组
#include <stdio.h> int main() { int arr1[] = {1,2,3,4,5}; int arr2[] = {2,3,4,5,6}; int arr3[] = {3,4,5,6,7}; //数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中 int* parr[3] = {arr1, arr2, arr3}; int i = 0; int j = 0; for(i=0; i<3; i++) { for(j=0; j<5; j++) { printf("%d ", parr[i][j]); } printf("\n"); } return 0; }
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。 上述的代码模拟出⼆维数组的效果,实际上并⾮完全是⼆维数组,因为每⼀⾏并⾮是连续的。
8. const引入
• const定义
const是C++中的一个关键字,表示常量,即表示变量的值是固定不变的。const可以用于变量、函数参数、函数返回值、类成员函数等。在变量中使用const,可以定义一个常量,该变量的值不能被修改。
• const语法
七. 一系列的指针变量
1. 字符指针变量
在指针的类型中我们知道有⼀种指针类型为字符指针 char* ;
int main() { char ch = 'w'; char *pc = &ch; *pc = 'w'; return 0; } //还有⼀种使⽤⽅式如下: int main() { const char* pstr = "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗? printf("%s\n", pstr); return 0; }
2. 数组指针变量
1. 数组指针概念
• 整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针。 • 浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。
数组指针变量 int (*p)[10]; 解释:p先和*结合,说明p是⼀个指针变量变量,然后指着指向的是⼀个⼤⼩为10个整型的数组。所以 p是⼀个指针,指向⼀个数组,叫 数组指针。 这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。
2. 数组指针初始化
int arr[10] = {0}; &arr;//得到的就是数组的地址 int(*p)[10] = &arr;
3. ⼆维数组传参的本质
#include <stdio.h> void test(int a[3][5], int r, int c) { int i = 0; int j = 0; 比特就业课主页:https://m.cctalk.com/inst/s9yewhfr 比特就业课 for(i=0; i<r; i++) 比特就业课主页:https://m.cctalk.com/inst/s9yewhfr { for(j=0; j<c; j++) { printf("%d ", a[i][j]); } printf("\n"); } } int main() { int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}}; test(arr, 3, 5); return 0; }
⼆维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维 数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。
第⼀⾏的⼀维数组的类型就是 int [5] ,所以第⼀⾏的地址的类型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址。tips:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。
4. 函数指针变量
1. 函数指针变量
• 创建函数指针变量
#include <stdio.h> void test() { printf("hehe\n"); } int main() { printf("test: %p\n", test); printf("&test: %p\n", &test); return 0; } //输出结果如下: test: 005913CA &test: 005913CA
打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过 &函数名 的⽅式获得函数的地址。
• 使用函数指针变量
如果我们要将函数的地址存放起来,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针⾮常类似。如下:
void test() { printf("hehe\n"); } void (*pf1)() = &test; void (*pf2)()= test; int Add(int x, int y) { return x+y; } int(*pf3)(int, int) = Add; int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的
• 解析函数指针变量
int (*pf3) (int x, int y) | | ------------ | | | | | pf3指向函数的参数类型和个数的交代 | 函数指针变量名 pf3指向函数的返回类型 int (*) (int x, int y)
2. typedef 关键字
typedef 是⽤来类型重命名的,可以将复杂的类型,简单化。
//⽐如,你觉得 unsigned int 写起来不⽅便,如果能写成 uint 就⽅便多了,那么我们可以使⽤: typedef unsigned int uint; //将unsigned int 重命名为uint //如果是指针类型,能否重命名呢?其实也是可以的,⽐如,将 int* 重命名为 ptr_t ,这样写: typedef int* ptr_t; //但是对于数组指针和函数指针稍微有点区别: //⽐如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写: typedef int(*parr_t)[5]; //新的类型名必须在*的右边 //函数指针类型的重命名也是⼀样的,⽐如,将 void(*)(int) 类型重命名为 pf_t ,就可以这样写: typedef void(*pfun_t)(int);//新的类型名必须在*的右边 //那么要简化代码2,可以这样写: typedef void(*pfun_t)(int); pfun_t signal(int, pfun_t);
5. 函数指针数组
数组是⼀个存放相同类型数据的存储空间,我们已经学习了指针数组, ⽐如:
int *arr[10]; //数组的每个元素是int*
把函数的地址存到⼀个数组中,这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[3])(); int *parr2[3](); int (*)() parr3[3];
答案是:parr1 parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢? 是 int (*)() 类型的函数指针
6. 转移表
函数指针数组的⽤途:转移表
• 举例:计算器的⼀般实现:
#include <stdio.h> int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int div(int a, int b) { return a / b; } int main() { int x, y; int input = 1; int ret = 0; do { printf("*************************\n"); printf(" 1:add 2:sub \n"); printf(" 3:mul 4:div \n"); printf(" 0:exit \n"); printf("*************************\n"); printf("请选择:"); scanf("%d", &input); switch (input) { case 1: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = add(x, y); printf("ret = %d\n", ret); break; case 2: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = sub(x, y); printf("ret = %d\n", ret); break; case 3: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = mul(x, y); printf("ret = %d\n", ret); break; case 4: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = div(x, y); printf("ret = %d\n", ret); break; case 0: printf("退出程序\n"); break; default: printf("选择错误\n"); break; } } while (input); return 0; }
• 使⽤函数指针数组的实现:
#include <stdio.h> int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a*b; } int div(int a, int b) { return a / b; } int main() { int x, y; int input = 1; int ret = 0; int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表 do { printf("*************************\n"); printf(" 1:add 2:sub \n"); printf(" 3:mul 4:div \n"); printf(" 0:exit \n"); printf("*************************\n"); printf( "请选择:" ); scanf("%d", &input); if ((input <= 4 && input >= 1)) { printf( "输⼊操作数:" ); scanf( "%d %d", &x, &y); ret = (*p[input])(x, y); printf( "ret = %d\n", ret); } else if(input == 0) { printf("退出计算器\n"); } else { printf( "输⼊有误\n" ); } }while (input); 49 return 0; }
八. qsort函数深入解析
1. qsort定义
2. qsort排序
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> struct Stu { char name[20]; int age; }; //比较两个字符串大小(按名字排序) int cmp_stu_1(const void* e1, const void* e2) { return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name); //strcmp返回值是小于0,等于0,大于0 。 // 比较的是对应字符Ascall码大小 } //按照年龄大小进行比较(按年龄排序) int cmp_stu_2(const void* p1, const void* p2) { return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age; } void print_arr(struct Stu* s, int sz) { for (int i = 0; i < sz; i++) { printf("%s %d\n", (s + i)->name, (s + i)->age); } printf("\n"); } void test() { struct Stu arr[3] = { {"张三",19},{"李四",21},{"王五",18} }; int sz = sizeof(arr) / sizeof(arr[0]); qsort(arr, sz, sizeof(arr[0]), cmp_stu_1); qsort(arr, sz, sizeof(arr[0]), cmp_stu_2); print_arr(arr, sz); } int main() { test(); return 0; }
3. qsort模拟实现
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> //交换函数 void Swap(char* buff1, char* buff2, size_t width)//char*一次仅访问一个字节,width:字节个数 { for (int k = 0; k < width; k++) { char tmp = *buff1; *buff1 = *buff2; *buff2 = tmp; buff1++; buff2++; } } //排序函数 void bubble_sort(void* base, size_t sz, size_t width, int(*int_cmp)(const void* p1, const void* p2))){ //初始函数指针/数组 , 数组元素个数 , 每个元素的大小 , 相邻两个元素的指针 for (int i = 0; i < sz - 1; i++) { for (int j = 0; j < sz - 1 - i; j++) { if (int_cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0) { //将前后两个数据进行交换 Swap((char*)base + j * width, (char*)base + (j + 1) * width, width); } } } } //比较函数 int int_cmp(const void* p1, const void* p2) { return *(int*)p1 - *(int*)p2; } //打印函数 //void print_arr(const void* p, size_t sz) void print_arr(int arr[], size_t sz) { for (int i = 0; i < sz; i++) { //printf("%d ", *((int*)(p + i))); printf("%d ", arr[i]); } } //主函数 int main() { int arr[] = { 5,6,4,7,3,8,2,9,1,0 }; int sz = sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, sz, sizeof(arr[0]), int_cmp); print_arr(arr, sz); return 0; }
九. sizeof与strlen
• sizeof
sizeof是单目操作符,不是函数,只关注内存类型,即空间大小。
tips:sizeof中如果有表达式,表达式不参与计算。
because-> sizeof操作时在编译时,而表达式进行时在程序运行时进行操作
• strlen
主要关心‘/0‘,参数是地址
仅用于求字符串长度,计算“\0”之前字符的个数,当遇见'\0'时,结束。
strlen 是C语⾔库函数,功能是求字符串⻓度。函数原型如下:
size_t strlen ( const char * str );
统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数。
strlen 函数会⼀直向后 找 \0 字符,直到找到为⽌,所以可能存在越界查找。
#include <stdio.h> int main() { char arr1[3] = {'a', 'b', 'c'}; char arr2[] = "abc"; printf("%d\n", strlen(arr1)); printf("%d\n", strlen(arr2)); printf("%d\n", sizeof(arr1)); printf("%d\n", sizeof(arr2)); return 0; }