目录
[一. const修饰指针](#一. const修饰指针)
[1. const 修饰指针](#1. const 修饰指针)
[1.1 const修饰变量](#1.1 const修饰变量)
[1.2 const 修饰指针变量](#1.2 const 修饰指针变量)
[2.1 指针 +- 整数](#2.1 指针 +- 整数)
[2.2指针-指针 (+号不行)](#2.2指针-指针 (+号不行))
2.3指针的关系运算(指针和指针比较大小,或者是地址和地址比较大小)
[三. 野指针](#三. 野指针)
[3. 野指针](#3. 野指针)
[2.1 野指针成因](#2.1 野指针成因)
[3.2 如何规避野指针](#3.2 如何规避野指针)
[3.2.1 指针初始化](#3.2.1 指针初始化)
[3.2.2 ⼩⼼指针越界](#3.2.2 ⼩⼼指针越界)
[2.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性](#2.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性)
[2.2.4 避免返回局部变量的地址](#2.2.4 避免返回局部变量的地址)
[四:assert 断⾔](#四:assert 断⾔)
[5.1 strlen的模拟实现](#5.1 strlen的模拟实现)
1. const 修饰指针
1.1 const修饰变量
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。 但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是const的作⽤。
代码如下:
cpp
int main()
{
const int a = 10; //使用了const进行修饰后,a此时具有了常属性(即不能被修改了),但本质上a还是变量
a = 20;//上面的a被const修饰后,此时这个代码会报错,但如果放在C++坏境(即.cpp文件)下,它会将a当成常量,因此不会报错,在C语言会报错
printf("%d\n", a);
return 0;
}
使用了const进行修饰后,a此时具有了常属性(即不能被修改了),但本质上a还是变量
上面的a被const修饰后,此时这个代码会报错,但如果放在C++坏境(即.cpp文件)下,它会将a当成常量,因此不会报错,在C语言会报错
输出结果如下:

那我们怎么才能打破这种局面呢?
利用指针可以打破上面的a的限制
代码如下:
cpp
int main()
{
const int a = 10;
int* p = &a;
*p = 0;
printf("a=%d\n", a);//此时的a还是被修改了,a=0
return 0;
}
输出结果如下:

我们可以看到这⾥⼀个确实修改了,但是我们还是要思考⼀下,为什么a要被const修饰呢?就是为了 不能被修改,如果p拿到a的地址就能修改a,这样就打破了const的限制,这是不合理的,所以应该让 p拿到a的地址也不能修改a,那接下来怎么做呢?
1.2 const 修饰指针变量
⼀般来讲const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不⼀样的、
const在修饰指针变量的时候如果放在*的右边
cpp
int main()
{
int a = 10;
int b = 20;
int * const p = &a; //const在修饰指针变量的时候如果放在*的右边,此时限制的是变量它本身,即p,此时我们的p不能再修改,但是我们可以修改它的指针变量指向的内容
p = & b; //错误
*p = 100;
printf("%d\n", a);
return 0;
}
此时会有个地方不能够更改了
p = & b; //错误
const在修饰指针变量的时候如果放在*的左边
cpp
int main()
{
int a = 10;
int b = 20;
int const * p = &a; //如果放在*的左边,那么情况反过来,即限制了指针指向的内容,但我们可以修改指针变量本身,即p
p = &b;
*p = 100; //错误
printf("%d\n", a);
return 0;
}
此时会有个地方不能够更改了
*p = 100; //错误
如果左右都有,那么即全部限制,此时都不能修改了
cpp
int main()
{
int a = 10;
int b = 20;
int const * const p = &a;//如果左右都有,那么即全部限制,此时都不能修改了
p = &b;//错误
*p = 123;//错误
return 0;
}
此时会有个地方不能够更改了
p = &b;//错误
*p = 123;//错误
结论:const修饰指针变量的时候
const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。
const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。
2.指数运算
2.1 指针 +- 整数
利用下标打印数组元素
cpp
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);//计算元素个数的固定公式,这里的 sz=10
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);//利用下标来打印数组里面的元素,比如arr[0]=1,依次循环
}
return 0;
}
输出结果:

利用指针打印数组元素
cpp
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0]; //这里可以直接写 arr 是一个意思
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", *p);//打印出数组里面的元素
p++;//换到下一个地址,比如第一个地址是 &arr[0];,输出的是1,这个打印完之后进入循环,到 &arr[1],输出的是2,就这样依次循环下去
}
return 0;
}
输出结果和上面一样
还有一个写法,就是将printf函数里面*p改成*(p+i),然后把p++去掉
cpp
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];//打印数组里面第一个元素1,这里的0是下标
// int* p = arr;//上面一行的代码也可以这样写,更加简洁,因为arr数组本身就是地址,所以不用加&
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", *(p+i));//打印出数组里面的元素
}
return 0;
}
2.2指针-指针 (+号不行)
计算的前提下是:两个指针指向了同一块空间!!!
计算原理:指针 - 指针 = 它们之间隔了多少步数(就是1到9要走8步才能从1变成9,是这个意思)
cpp
int main()
{
int arr[10] = { 0 };
printf("%zd\n", &arr[9] - &arr[0]);//输出9,大的减小的是一个>0的数
printf("%zd\n", &arr[0] - &arr[9]);//输出-9
return 0;
}
错误的写法
cpp
指针 - 指针的计算前提是:两个指针指向同一块(数组)空间!!!
int main()
{
int arr[10] = { 0 };
char ch[5] = { 0 };
printf("%zd\n", &ch[4] - &arr[6]);
return 0;
}
原因是:指针的计算前提是:两个指针指向同一块(数组)空间!!!
正常方式求字符串的长度
#include<string.h> // ---strlen函数要使用的头文件
cpp
int main()
{
char arr[] = "abcdef";
int len = strlen(arr);
printf("%d\n", len); //输出6
return 0;
}
输出结果:

利用指针求字符串的长度
cpp
int my_strlen(char* str) //地址要使用指针来接受,然后创建指针变量str
{
int count = 0;
while (*str != '\0') //'\0'是字符串结束标志
{
count++; // 记录指针移动了多少步
str++; //让指针指向下一个字符
}
return count;
}
//注意: strlen函数只会计算\0之前的数
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr); //数组名arr是数组第一个元素的地址,即arr == &arr[0]
printf("%d\n", len);// 输出6
return 0;
}
输出结果和上面一样
利用指针 - 指针 来计算字符串的长度
cpp
int my_strlen(char* str)
{
char* start = str; //str自动转为指针,因此前面不用加&,只有当遇到普通变量的时候才要加&
while (*str != '\0')
{
str++;
}
return str - start; //利用指针 - 指针 来计算长度
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr); //数组本身就是地址
printf("%d\n", len);// 输出6
return 0;
}
输出结果一样是6
2.3指针的关系运算(指针和指针比较大小,或者是地址和地址比较大小)
利用指针关系打印数组
cpp
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);//计算数组里面的元素个数,即10个
int* p = arr;// arr 相当于 &arr[0] 一样的意思,不同的写法
while (p < arr + sz)//当指针p还没有到达数组末尾的时候,继续循坏
{
printf("%d ", *p);
p++; //让指针指向下一个元素,即下标arr[1],对应元素2,依次往后推
}
return 0;
}
输出结果如下:

3. 野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
2.1 野指针成因
- 指针未初始化
cpp
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
- 指针越界访问
cpp
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i = 0; i <= 11; i++)
{
*(p++) = i;
}
return 0;
}
- 指针指向的空间释放
cpp
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
上面的代码全部都是野指针,会报错误的
3.2 如何规避野指针
3.2.1 指针初始化
如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址 会报错
cpp
int main()
{
int a = 10;
int* p1 = &a;
*p1 = 20;
int* p2 = NULL; //空指针
*p = 200; //此时会报错
return 0;
}
此时的结果会报错误

3.2.2 ⼩⼼指针越界
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。
2.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的 时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL。
我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来, 就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起 来
不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我 们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去 使⽤
2.2.4 避免返回局部变量的地址
如造成野指针的第3个例⼦,不要返回局部变量的地址。
四:assert 断⾔
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报 错终⽌运⾏。这个宏常常被称为"断⾔"。
assert(p != NULL);
上⾯代码在程序运⾏到这⼀⾏语句时,验证变量p 是否等于 NULL,如果确实不等于 NULL ,程序继续运⾏,否则就会终⽌运⾏,并且给出报错信息提⽰
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), 任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), 流 st assert() 不会产⽣ assert() 就会报错,在标准错误 derr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的⽂件名和⾏号。
assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:它不仅能⾃动标识⽂件和 出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问 题,不需要再做断⾔,就在 #include 语句的前⾯,定义⼀个宏 NDEBUG
#define NDEBUG
#include<assert.h>
代码如下:
cpp
int main()
{
int a = 10;
int* p = NULL;
assert(p != NULL);
*p = 20;
printf("%d \n", *p); //此时会报错误,并且会将具体的错误显示出来,这就是assert断言的好处
return 0;
}
输出结果如下:

然后,重新编译程序,编译器就会禁⽤⽂件中所有的 除这条 assert() 语句。如果程序⼜出现问题,可以移 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语 句。
assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。
⼀般我们可以在 Debug 中使⽤,在 发环境中,在 在 Re Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题, lease 版本不影响⽤⼾使⽤时程序的效率。
assert还可以断言其他的表达式(非指针),不一定非要指针
cpp
int main()
{
int n = 0;
scanf("%d", &n);
assert(n > 3); //只有我们输入的值大于3就不会断言
return 0;
}
如何关掉assert呢?
cpp
#define NDEBUG // 我们只需在assert的头文件的上面加上这个代码就行了
#include<assert.h>
int main()
{
int n = 0;
scanf("%d", &n);
assert(n > 3); // 此时我们可以随便输入值,不会出现断言了,因为已经关闭了
return 0;
}
只需这样:
#define NDEBUG // 我们只需在assert的头文件的上面加上这个代码就行了
#include<assert.h>
五:指针的使⽤和传址调⽤
5.1 strlen的模拟实现
库函数strlen的功能是求字符串⻓度,统计的是字符串中 函数原型如下:
size_t strlen ( const char * str );
参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回⻓度。 如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 到 \0 就停⽌。
参考代码如下:
cpp
#include<assert.h>
8.指针的使用和传址调用
8.1 strlen的模拟实现
int my_strlen(const char* str) //const能够防止 *str 被更改
{
int count = 0;
assert(str != NULL);
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%zd\n", len);
return 0;
}
输出结果:
传值调用和传址调用
5.2传值调用和传址调用
问题:写一个函数,交换两个整数的内容
cpp
void Swap2(int* pa, int* pb)
{
int tmp = 0;
tmp = *pa; // tmp=a
*pa = *pb; // a=b
*pb = tmp; // b=tmp
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a= %d b=%d \n", a, b);
Swap2(&a, &b); //传址调用,将a和b的地址传过去
printf("交换后:a= %d b=%d \n", a, b);
return 0;
}
输出结果如下:
