C/C++指针详解(超详细教程!!!)
指针详解1
1. 内存和地址
1.1 内存
在了解内存和地址之前,先讲一个生活的中的案例
假设我们要买一味中药龙胆,医生就会根据中医柜子上的标签来找到龙胆。如果没有标签的话,医生就会一个个找,效率很低。
有了标签,医生可以快速的这味中药。
上面的例子,相当于我们计算机中的内存单元存放是一样的。
计算器把内存划分为多个的内存单元,每个内存单元的⼤⼩取1个字节。
每个内存单元有相对的编号,如同中医柜子上的标签号。
计算机中常⻅的单位(补充):
⼀个⽐特位可以存储⼀个2进制的位1或者0
在计算机中我们把内存单元的编号也称为地址。C语⾔中给地址起了新的名字叫:指针。
所以我们可以理解为:内存单元的编号 == 地址 == 指针
1.2 地址总线
CPU和内存之间有大量的数据进行交互,所以两者必须用线连接起来
在32位机器上有32根地址总线,每根线只有两态,表⽰0,1【电脉冲有无】,那么⼀根线,就能表⽰2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表2^32种含义,每⼀种含义都代表⼀个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊CPU内寄存器。
2. 指针变量和地址
2.1 取地址操作符
在C语言中创建变量就是向内存申请空间。比如:
上述的代码就是创建了整型变量a,内存中申请4个字节,⽤于存放整数10
我们是怎么取到a的地址呢?
用到一个操作符(&)—取地址操作符
/* 向内存申请空间 存放变量a的值*/
int a = 10;
&a;/* 取出a的地址 */
2.2 指针变量和解引用操作符(*)
2.2.1 指针变量
我们通过取地址操作符(&)拿到的地址是⼀个数值,这个数值有时候也是需要
存储起来,⽅便后期再使⽤的,我们把这样的地址值存放在指针变量中
/* 向内存申请空间 存放变量a的值*/
int a = 10;
&a;/* 取出a的地址 */
int* pa = &a;
以上代码是将a的地址存放到指针变量pa中。
指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的数值都会理解为地址。
2.2.2 理解指针变量
int* pa = &a;
这⾥pa左边写的是 int* , * 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向的是整型(int)类型的对象。
char类型变量地址的存放
char c = 'w';
&c;/* 取出char类型变量的地址 */
char* pc = &c;
这⾥pc左边写的是char* , * 是在说明pv是指针变量,⽽前⾯的 char 是在说明pv指向的是字符类型(char)类型的对象。
2.2.3 解引用操作符
上面我们已经知道指针的存放了,那指针该怎么使用呢?
在C语言中,我们我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这⾥必须学习⼀个操作符叫解引⽤操作符(*)。
char c = 'w';
&c;
char* pc = &c;
*pc = 'u';/* 这里的*就是解引用操作符 */
上述代码中的*pc意思就是通过pc中存放的地址,找到指向的空间,
pc其实就是v变量了;所以pc = ‘u’,这个操作符是把c改成了字符‘u’
2.3 指针变量的大小
前面已经提到过了,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储
如果指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要
8个字节的空间,指针变的大小就是8个字节。
指针变量的⼤⼩取决于地址的⼤⼩
在32位平台的机器上地址是32bit位,指针大小为4字节。
在64位平台的机器上地址是64bit位,指针大小为8字节。
注意指针变量的大小和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。
3.指针变量类型的意义
3.1指针的解引用
指针的类型决定对指针解引⽤的时候有多⼤的权限(也就是⼀次能访问几个字节)。
⽐如: char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。
3.2指针加减整数
指针加减整数
char*+1——>跳过char(1)类型个字节
int*+1——>跳过int(4)类型个字节
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)
4.const修饰指针
4.1const修饰变量
变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。但是如果这个指针变量被const所修饰就无法改变。这就是const的作用。
4.2const修饰指针变量
const修饰指针变量的时候,只要记住以下两个结论就行:
1.const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。
2.const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变
5.指针运算
指针的基本运算有三种,分别是:
• 指针± 整数
• 指针-指针
• 指针的关系运算
5.1 指针± 整数
前面已经讲过了,这里就不多赘述。
指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)
5.2 指针-指针
指针减指针是指两个指针相减的操作。当两个指针指向同一数组(或同一块内存)中的不同元素时,可以通过将它们相减来得到它们之间的元素个数(或者是它们之间的偏移量)。
指针减指针,得到的是中间的元素个数(偏移量)
6.野指针
野指针的概念:
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1造成野指针的原因
1.指针未初始化
2.指针越界访问
3.指针指向的空间释放
6.2如何避免野指针
1.指针初始化
如果不知道指针应该指向哪⾥,可以给指针赋值NULL.NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错。
2.避免指针越界访问
3.指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
7.assert断言
assert断言⽤于在运⾏时确保程序符合指定条件,如果不符合,就报
错终⽌运⾏。这个宏常常被称为“断⾔”。头文件是#include<assert.h>
assert断言的好处:它不仅能⾃动标识⽂件和出问题的行号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。
#define NDEBUG
#include<assert.h>
8.指针的使用和传址调用
8.1传值调用和传址调用
传值调用—>把变量本身的值传递给函数,这样无法通过形参来改变实参的值。
实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参。
传址调用—>把变量本身的地址传递给函数,这样可以通过形参来改变实参的值。
指针详解2
1.数组名的理解
1.数组名的理解
数组名本来就是地址,⽽且是数组⾸元素的地址。
int arr[] = { 1,2,3,4,5,6,7 };
printf(" arr = %p\n",arr);/* arr和&arr[0的地址一致 */
printf("&arr[0] = %p\n", &arr[0]);
数组名就是数组⾸元素(第⼀个元素)的地址是对的,但是有两个例外:
1.sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的大小,单位是字节。
2. &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)。
除此之外,任何地方使用数组名,数组名都表示⾸元素的地址。
int main()
{
int arr[] = { 1,2,3,4,5,6,7 };
printf(" arr = %p\n", arr);/* arr和&arr[0的地址一致 */
printf(" arr+1 = %p\n", arr+1);/* arr和&arr[0的地址一致 */
printf(" &arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0]+1);
printf(" &arr = %p\n", &arr);/* 这里整个数组的地址 */
printf(" &arr+1 = %p\n", &arr + 1);/* 这里跳过数组的地址 */
printf(" arr[0] = %zd\n", sizeof(arr[0]));/* 只计算数组中一个元素的大小 */
printf(" arr = %zd\n", sizeof(arr));/* 计算整个数组的大小 */
return 0;
}
运行结果如下
其中&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是⾸元素的地址,+1就是跳过⼀个元素的大小。
但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的大小。
2.使用指针访问数组
有了前面的知识,再加上一些的数组的基础知识,我们就可以使用指针访问数组。
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);/* 求元素个数 */
int* parr = arr;
int i = 0;
for (i = 0; i < sz; i++)
{
/* 遍历数组 */
//printf("%d ", *(parr + i));
printf("%d ",parr[i]);
}
return 0;
}
分析:
1.数组名arr是数组⾸元素的地址,可以赋值给parr,其实数组名arr和parr在这⾥是等价的。
2.将*(parr+i)换成parr[i]也是能够正常打印的,所以本质上parr[i] 是等价于 *(parr+i)。
3.同理arr[i] 应该等价于 *(arr+i)
3.一维数组传参的本质
/* void test(int* arr) */ //指针的形式
void test(int arr[])
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[] = {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;
}
输出结果:
函数内部没有正确求出数组的大小,我们已经知道:数组名是数组首元素的地址;在数组的传递时把数组的地址给传过去的。本质上数组传递的是数组⾸元素的地址。多所以在函数内部需要一个指针变量来接受首元素的地址。
sizeof(arr) 计算的是⼀个地址的大小(单位字节)并不是数组的大小(单位字节)。
⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
4.冒泡排序算法
冒泡排序的核心思想:两两相邻的元素进行比较
void Bubble_sort(int arr[], int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz-1; i++)
{
int flag = 1;//假设已经有序
for (j=0;j<sz-1-i;j++)
{
//进行比较
if (arr[j] > arr[j + 1])
{
//进行交换
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = 0;//当flag=0;时,说明是无序的
}
}
if (flag == 1)
{
break;//这⼀趟没交换就说明已经有序,后续⽆序排序了
}
}
/* 打印 */
printf("排序后");
for (i = 0; i < sz; i++)
{
printf("%d ",arr[i]);
}
}
int main()
{
/* 冒泡排序 */
int arr[] = {3,2,6,5,1,4,9,8,7,0};
int sz = sizeof(arr)/sizeof(arr[0]);
int i = 0;
printf("排序前");
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
Bubble_sort(arr,sz);
return 0;
}
5.二级指针
指针变量也是变量,是变量就有地址,二级指针用来接收这些地址。
int main()
{
int a = 100;
int* pa = &a;
int** ppa = &pa;
return 0;
}
对于⼆级指针的运算有:
*ppa 通过对ppa中的地址进⾏解引⽤,这样找到的是 pa , *ppa 其实访问的就是 pa
6.指针数组
指针数组—是存放指针的数组。
数组指针的每个元素都是⽤来存放地址(指针)的。
数组指针的每个元素是地址,⼜可以指向⼀块区域。
7.指针数组模拟二维数组
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[] = {arr1,arr2,arr3};/* 将三个数组的地址存放指针数组parr中 */
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]就是整型⼀维数组中的元素。
指针详解3
1.字符指针变量
在指针的类型中我们知道有⼀种指针类型为字符指针 char*
下面有一道笔试题:
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
输出结果:
str1和str2分别指向不同的地址,所以str1和str2不同
str3和str4指向的是同⼀个常量字符串(也就是同一篇内存空间)。C/C++会把常量字符串存储到单独的⼀个内存区域,当⼏个指针指向同⼀个字符串的时候,所以str3和str4会指向同⼀块内存。
数组指针变量
2.1数组指针变量概念
1.指针数组—是存放指针的数组。
2.整型指针变量—存放的是整形变量的地址,能够指向整形数据的指针。
3.浮点指针变量—存放浮点型变量的地址,能够指向浮点型数据的指针。
4.数组指针变量—存放的应该是数组的地址,能够指向数组的指针变量。
书写格式:
int(*pa)[10];
p先和结合,说明p是⼀个指针变量变量,然后指向的是⼀个⼤⼩为10个整型的数组。所以p是⼀个指针,指向⼀个数组,叫 数组指针。
这⾥要注意:[]的优先级要⾼于号的,所以必须加上()来保证p先和*结合。
2.2数组指针变量初始化
数组指针变量是⽤来存放数组地址的。
int main()
{
/* 数组指针变量初始化 */
int arr[10] = {0};
int(*parr)[10] = &arr;
return 0;
}
调试:
调试可以看到&arr和parr是完全相同的。
数组指针类型解析:
3.二维数组传参的本质
二维数组的传参是这样写的
void test2(int arr[3][5], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf("%d ",arr[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} };
test2(arr, 3, 5);
return 0;
}
输出结果:
注意:二维数组的创建和传参行可以省略,但是列不可以省略。
根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。⼆维数组传参本质上也是传递了地址,传递的是第⼀
⾏这个⼀维数组的地址。
总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。
4.函数指针变量
函数指针变量应该是⽤来存放函数地址的,未来通过地址能够调⽤函数的。
void test3()
{
;
}
int main()
{
printf(" test = %p\n",test);
printf("&test = %p\n", &test);
return 0;
}
输出结果:
函数是有地址的,函数名就是函数的地址,当然也可以通过 &函数名 的方式获得函数的地址。
要把函数的地址存放起来,就要创建函数指针变量。函数指针变量的写法其实和数组指针非常类似。
书写格式:
void (*p)();/* 函数指针变量 */
函数指针类型解析:
4.2函数指针变量的使用
通过函数指针调⽤指针指向的函数:
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*p)(int, int) = Add;
printf(" p = %d\n",p(3,4));
printf("(*p) = %d\n", (*p)(3, 4));/* 把3,4传参给函数Add */
return 0;
}
输出结果:
4.3typedef关键字
typedef 是⽤来类型重命名的,可以将复杂的类型,简单化。
比如 unsigned int 可以写成 uint:
typedef unsigned int uint;//将unsigned int 重命名为uint
5.函数指针数组
把函数的地址存放到一个数组中,这个就叫函数指针数组。
int(*p[3])();/* 函数指针数组 */
分析:
parr1 先和 [] 结合,说明 parr1是数组,数组是 int (*)() 类型的函数指针。
6.转移表
函数指针数组的⽤途:转移表
void meun()
{
printf("------------计算器------------\n");
printf("******** 1.Add 2.Sub *******\n");
printf("******** 3.Mul 2.Div *******\n");
printf("******** 0.exit *******\n");
printf("------------计算器------------\n");
}
int Add(int x, int y)/* 加法函数 */
{
return x + y;
}
int Sub(int x, int y)/* 减法函数 */
{
return x - y;
}
int Mul(int x, int y)/* 乘法函数 */
{
return x * y;
}
int Div(int x, int y)/* 除法函数 */
{
return x / y;
}
int main()
{
/* 转移表 */
meun();
int input = 0;
int x = 0;
int y = 0;
int(*parr[5]) (int, int) = { 0,Add,Sub,Mul,Div };
do
{
printf("请选择:>");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("请输入两个操作数:>");
scanf("%d%d", &x, &y);
int ret = parr[input](x, y);//根据input的值来选择函数
printf("运算结果:%d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
break;
}
else
{
printf("输入错误,重新输入\n");
}
} while (input);
return 0;
}
指针详解4
4.1.回调函数
回调函数就是⼀个通过函数指针调⽤的函数。
如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数时,被调⽤的函数就是回调函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。
4.2.qsort库函数
qsort库函数—可以排序任意类型元素的数据。
对于qsort库函数函数的详解可以看这篇博客:C语言实现用冒泡排序实现qsort函数
4.3.sizeof和strlen的对比
4.3.1sizeof操作符
- sizeof 计算变量所占内存内存空间⼤⼩的,单位是字节,如果操作数是类型的话,计算的是使⽤类型创建的变量所占内存空间的大小。
sizeof 只关注占⽤内存空间的⼤⼩,不在乎内存中存放什么数据。
4 3.2 strlen库函数
- strlen 是C语⾔库函数,功能是求字符串⻓度。函数原型如下:
size_t strlen ( const char * str );
strlen统计的是从 strlen 函数的参数 str 中这个地址开始向后, \0 之前字符串中字符的个数。strlen 函数会⼀直向后找 \0 字符,直到找到为⽌,所以可能存在越界查找。
4.4sizeof和strlen的对比
4.4.1 sizeof
- sizeof是操作符
- sizeof计算操作数所占内存的⼤⼩,单位是字节
- 不关注内存中存放什么数据
4.4.2 strlen
- strlen是库函数,使⽤需要包含头⽂件 string.h
- srtlen是求字符串⻓度的,统计的是 \0 之前字符的隔个数
- 关注内存中是否有 \0 ,如果没有 \0 ,就会持续往后找,可能
会越界
数组名的意义(重要)
数组名的意义:
- sizeof(数组名),这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩。
- &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址。
- 除此之外所有的数组名都表⽰⾸元素的地址。
总结
花了近五天的时间,从学习指针到完这篇博客。对于C/C++来说指针不难,其实还是一句话: 当你还在抱怨没有鞋穿,回头看看没有脚的那些人。
学海无涯,永无止境。
希望这篇博客能帮到你。