目录
1.如何理解指针
内存&&地址
内存单元的编号==地址==指针
C语言中地址就是存储在指针变量当中的,指针就是存储地址的变量。
创建变量的本质:像内存申请一块空间
e.g:int a = 10;//向内存申请了一块四个字节的空间,每个字节均有地址(但&a取的是地址值较小的字节的地址)
内存划分地址的最小单元:字节/byte 1 byte = 8 bit
编址
计算机的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的,可以类比琴弦——音符。
关于地址总线的简单理解:
32位机器有32根地址总线,每根线只有两种状态,表示0,1(电脉冲有无)。因此,一根线最多能表示两种含义,32根线就能表示2^32种含义,每一种含义都代表一个地址。
地址信息被下达给内存,内存上就可以找到该地址对应的数据,再通过数据总线传入传入CPU内寄存器。
2.指针变量和地址
取地址操作符——&
e.g. int a = 6;
printf("%p",&a);
取出&&打印的就是a 的 最小的 那个地址(int 类型占四字节,有四个地址)
知道第一个字节地址,剩下的的地址也可以被找到
指针变量&&解引用操作符(*)
通过 取地址操作符 拿到的地址是一个数值,有时候为了 保留存储 此数值
我们将他存储到 指针变量 中
理解指针变量:
e.g. int a = 10;
int * pa = &a;
* ——说明 pa 是指针变量;
int ——说明 pa 指向整型(int)类型的对象。
以此类推,按 指向对象 分类,(一维)指针变量主要有以下几种类型:
int *
char *
float*
double*……
数组指针:指向数组的指针
类型: "数组存储元素类型 (*)[数组元素个数]"
void* 不指向任何类型的指针,可用来接受各种类型的地址,使用其进行操作时需要强制类型转换。
函数指针:指向函数的指针
类型: "函数返回类型 (*)(函数参数)"
void* —— 无具体类型的指针(泛型指针)
这种类型的指针可以用来接受任意类型的地址。但它也有局限性:它不能直接进行指针的“±整数”和 解引用 的运算,因为其没有具体的类型,不确定能访问几个字节。
泛型指针的应用:用于函数参数部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得一个函数来处理多种类型的数据。
指针类型的意义:
①决定指针进行解引用操作时一次能访问几个字节;
②决定了指针±1时,一次跨过几个字节。
3.const 修饰指针变量
当const修饰一个变量时,
e.g. const int a = 10;
这时变量a就具有了常属性(i.e.不能直接被修改了)但它本质上还是变量,称之为常变量。
此时我们无法通过直接赋值来改变 a 存放的值,需要通过指针变量解引用修改a;
int * p = &a;
如果const修饰指针变量又会怎么样呢?
①const放在“ * ”右边:限制指针变量本身的指向,指针变量不能再指向其他变量,但我们可以通过指针变量修改其所指变量的值;(左边定向)
②const放在“ * ”左边:限制的是指针所指向的内容,即不能通过指针修改它所指向的变量的值,但我们可以修改指针变量自身的值,即修改它的指向。(右边定值)
当然,也可以在“ * ”两边各放一个,彻底锁死。
4.指针运算
分三类:
指针+/-整数 指针+/-指针 指针的关系运算
1.指针+/-整数
type* p;
p±1 ——> 跳过 1*sizeof(type)
常用于访问数组
2.指针+/-指针
前提:参与运算的指针必须是同类型指针,否则报错;
指针之间相加并无意义,指针相减(这里专指大-小)得到的是两个指针之间的元素个数
3.指针的关系运算
指针变量 与 指针变量比较大小 <=> 地址和地址比较大小
e.g.可以用指针的关系运算遍历数组
5.野指针
这类指针变量指向的位置不可知(随机,不正确,无明确性质)
1.野指针成因
1.指针未初始化;
2.指针越界访问;
3.指针指向的空间已经被释放;
2.如何规避野指针
1.指针初始化
2.小心指针越界
3.指针变量不再使用时,及时置NULL
6.assert断言
头文件<assert.h>定义了宏assert<>;
用于运行时确保程序符合指定条件。如果不符合则报错并终止程序运行;
assert VS if~else
assert优点:
1.自动标识 文件 && 出问题的行号;
2.有一种无需更改代码,即可开启or关闭的 assert()的机制。
即:如果已确定程序没有问题,不再需要断言,就在#include<assert.h>语句的前面,定义一个宏NDEBUG
#define NDEBUG
#include<assert.h>
然后重新编译程序,编译器就会禁用文件中所有的assert()语句。如果程序又出现问题,将这条语句移除,再次编译即可。
assert缺点:
引入了额外的检查,增加了程序的运行时间。
Tip:一般我们可在Debug版本中使用,而在Release版本中选择禁用assert就行。在VS集成开发环境中的Release版本中,assert会被自动优化掉。
7.数组名的理解
一般情况下,arr<==>&arr[0]
除了两种情况:
1.sizeof(数组名),此时数组名代表整个数组;
2.&数组名,表示整个数组的地址(类型为数组指针)。
8.使用指针访问数组
注意两点:
1.数组元素在内存中是连续存放的,数组大小和数组元素 以及 元素类型均有关系;
2.下标应用操作符的灵活使用:e.g. 对于一个数组int arr[10],以及一个指向它的首元素的指针变量int * p = arr,arr[i] <==> p[i] <==> *(p+i) <==>*(arr+i)
arr[i] <==> i[arr] / *(arr + i)<==> *(i + arr)(了解即可)
9.一维数组传参
本质:传递的是数组首元素的地址
一维数组传参,形参部分可以写成数组的形式,也可以写成指针的形式。
10.二级指针
顾名思义,是指向指针变量地址的指针变量
e.g.
int a = 10;
int * pa = &a;(一级指针)
int **ppa = &pa;(二级指针)
除此之外并无不同。三级指针、四级指针……同理。
11.指针数组
是存放指针的数组,注意不要与 数组指针 混淆。
这种数组的每个元素都是用来存放地址(指针)的。
每个元素又可以指向不同的区域。
指针数组模拟二维数组
让一个指针数组中的每个元素都指向 存储同一类型,存储元素个数相等 的数组即可。
模拟毕竟只是模拟。与正经的二维数组比,它的每一行元素都不是连续存放的,不能像普通数组那样仅靠数组第一个元素的地址 ± 整数 找到所有元素。
12.字符指针变量
指针类型:char*
用法1
char ch = 'a';
char* p = &ch
用法2:
char* p = "abcdefg";
注意:
C/C++会把 常量字符串 存储到单独的一块区域, 几个指针指向同一个字符串时,他们实际会指向同一块内存。
但是用同一常量字符串去初始化不同的数组时就会开辟出不同的内存块。
13.数组指针变量
类型在前面交代过。
补充一点:[] 优先级 > * ,必须加上()保证 变量名与* 优先结合
数组指针初始化:
标准形式:int (*p) [10] = &arr;
14.二维数组传参(结合数组指针)
本质:传递的也是地址,是二维数组首元素的地址,即二维数组第一行这个一维数组的地址。
形参既可以写成指针形式,也可以写成数组形式
15.函数指针变量
顾名思义,指向函数的变量
函数也是有地址的,函数名就是函数的地址,我们可以由 "printf() 打印%p" 验证。我们可以通过
&函数名来得到函数地址。
函数指针类型的表示见上文。(与数组指针搭配记忆)
函数指针变量的使用:
即通过函数指针调用其所指向的函数
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int ,int) = Add;
printf("%d\n",(*pf)(2,3));
printf("%d\n",(*pf)(3,5));
return 0;
}
16.typedef关键字
用于类型重命名
typedef unsigned int u_int;
将数组指针 与 函数指针重命名:
//数组指针类型int(*)[5] 重命名为 parr_t
typedef int (*parr_t)[5];//新的类型名必须在*右边
//函数指针void(*)(int) 重命名为 pfun_t
typedef int (*pfun_t)(int);//新的类型名必须在*右边
17.函数指针数组
把函数指针存储到一个数组中
//以下代码做了简化
int (*pfarr[4])(int,int) = {Add,Sub,Mul.Div};//分别存储了+ - × ÷ 四个函数
//pfarr 即为函数指针数组
Tip:注意保证这些函数的地址的指针类型都是一致的(i.e.函数本身形式一致,包括参数返回类型等)
18.转移表——函数指针数组的应用
e.g.计算器:完成整数的加减乘除,自选计算功能
#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);
return 0;
}
19.回调函数
是一个通过函数指针调用的函数
将相似代码抽象成函数。有了函数指针后,函数的调用,可以使用函数名调用,也可使用函数指针调用。
e.g. 对于前面实现的计算器代码,可以写一个回调函数替换switch语句中冗余的语句:
void calc( int(*)pf(int,int) )
{
int x, y;
int ret = 0;
printf("输入操作数:");
scanf("%d %d",&x,&y);
ret = pf(x,y);
printf("ret = %d\n",ret);
}
//case只需要调用cal(add),cal(sub)……就可以了
20.qsort()的使用
是库函数,可直接用来排序数据,底层使用的是快速排序的方式,它可以排列任意类型的数据
头文件:<stdlib.h>
原型:void qsort(void*base,size_t num,size_t size,int(*compar)(const void*));
参数解释(从左到右):
1.指针,指向待排序数组的第一个元素
2.base指向的待排序数组的元素个数
3.base指向的待排序的数组的元素的大小
4.函数指针,指向的是两个元素的比较函数,需要使用者自己写。(把两个元素比较的方法,封装成函数,再把该函数的地址传递给排序函数)
e.g.使用回调函数,模拟实现qsort(采用冒泡方式)
int cmp_int(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2;
}
//width —— 一个元素的长度
void bubble_sort(void* base, size_t sz, size_t width, int (*cmp)(cosnt void* p2))
{
//趟数
int i = 0;
for(i = 0; i < sz - 1; i++)
{
//一趟内部两两比较
int j = 0;
for(j = 0; j < sz - 1 - i; j++)
{
//比较arr[j] 和 arr[j + 1]
if(cmp((char*)base + j*width,(char*)base + (j + 1)*width > 0)
{
//让cmp函数去比较。如果第一个地址只想元素的值大于第二个的,则交换值
//交换两个元素
Swap((char*)base + j * width, (char*)base + (j + 1) *width, width);
}
}
}
}
void Swap(char* buf1,char* buf2,size_t width)
{
int i = 0;
for(i = 0; i < width; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++,buf2++;
}
}
int main()
{
int arr[] = {3,1,7,8,5,2,4,9,0,6);
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr,sz,sizeof(arr[0]),cmp_int);
print_arr(arr,sz);//函数定义略
}
Swap函数交换的原理:将一大块空间切成一小块一小块(一个字节)交换。这是为了满足不同类型数据数据的交换,故我们采用最小单元一个字节进行交换。
本人能力有限,如有错误敬请指正!