指针¶
指一类存储 内存地址 的变量
指针的基类型¶
告诉编译器指针指向的 内存块大小 和 编码方式
一般存的数据是什么类型,基类型就是什么类型
比如:
int val=65;
int * p=&val; // p 存的是 val 所在的内存块第一个字节的地址
// int 告诉编译器:解引用时,按 int 大小往后取 4 字节并按 int 的编码方式翻译
指针算术¶
指针的偏移¶
合法的偏移运算符有:+
、-
、++
、--
、+=
、-=
如果没有指定偏移量,则默认根据基类型指定偏移规则
int * p=&a; // p+1 加 sizeof(int) = 4 Byte
也可用 [n]
自行指定偏移量
偏移量 \(=\) \(n×\mathrm{sizeof(}\) 基类型 \()\)
int (*p)[2]=&a; // 每次偏移 2*sizeof(int) = 8 Byte
char (*pp)[8]=&a; // 每次偏移 8*sizeof(char) = 8 Byte
Warning
void*
指针的偏移在不同平台上可能有差别,尽量避免使用
如:ANSI C
规定 void*
指针不能偏移;而 GNU
规定 void*
指针的偏移规则与 char*
完全相同
注意与指针数组区别
int (*p)[8]=&a; // 指针
int *p[8]; // 指针数组
Note
不建议将 int (*p)[8]
称作 数组指针,否则容易以为 sizeof(p)=8*sizeof(int)
指针的比较¶
比较两指针所存地址的 相对大小
int a[3][2];
*p1=&a[1][1];
*p2=&a[1][2];
*p3=&a[2][2];
// 则有 p1 < p2 < p3
两指针相减¶
只有 基类型相同 的指针才能相减,得到两指针间 基类元素个数,类型为 long int
,符号表示两指针相对位置,大减小为正
int a[5]={1,2,3,4,5};
int *p1=&a[1];
int *p2=&a[3];
printf("%ld %ld",p2-p1,p1-p2); // Result: 2 -2
指针的强制类型转换¶
格式:(基类型(指针级数)[偏移量])待转换指针
Note
指针级数:几级指针就是几个 *
,比如,普通指针就是一个 *
,指向指针数组的指针就是 **
int a[5]={1,2,3,4,5};
char (*p)[4]=(char(*)[4])a;
printf("%d %d\n",*(int*)p,*(int*)(p+3)); // Result: 1 4
return 0;
Warning
上面的程序有两点需要注意:
-
赋值运算符
=
左右两边类型要相同 -
编译器是根据基类型,而不是偏移量决定数据解释方式。也就是说,不会因为
p
的偏移量为4
就按int
类型解释*p
,所以在printf
中一定要告诉编译器*(int*)p
,才会按int
类型解释
指针与数组名¶
再次强调,数组名 ≠ 指向数组首地址的指针
相同¶
数组名和指针有许多相似之处,比如:
数组名可以当做指针访问数组中的元素,偏移量为数组中 单个元素所占的内存大小
int arr[5]={1,2,3,4,5};
printf("%d %d\n",arr[0],*arr); // Result: 1 1
printf("%d %d\n",arr[2],*(arr+2)); // Result: 3 3
区别¶
但指针与数组名也有许多不同:
-
sizeof
运算符\(\mathrm{sizeof}(\) 指针 \()=\) 指针变量所占内存大小
\(\mathrm{sizeof}(\) 数组名 \()=\) 数组所占内存大小
Note
一般来说,指针变量所占内存大小一般等于当前 CPU 的最大位数
int arr[5]={1,2,3,4,5}; printf("%lu\n",sizeof(arr)); // Result: 20 = 5 * sizeof(int) int* pi=nullptr; double* pd=nullptr; char* pc=nullptr; void* pv=nullptr; printf("%lu\n",sizeof(pi)); // Result: 8 printf("%lu\n",sizeof(pd)); // Result: 8 printf("%lu\n",sizeof(pc)); // Result: 8 printf("%lu\n",sizeof(pv)); // Result: 8
对于二维数组
int arr[2][2]={1,2,3,4}; printf("%lu\n",sizeof(arr)); // Result:16 printf("%lu\n",sizeof(arr[0])); // Result: 8
可以看到
arr[i]
实际上就是取出了arr
的第i
行 -
对数组名取地址
对数组名取地址会得到一个 指向数组首地址的常量指针,并且这个指针的 偏移量等于数组所占内存大小 ,也就是
&str
会得到一个类型为const char(*)[sizeof(str)]
的指针也可以认为,数组名并不是一个实体的变量,没有被分配内存(不像指针变量,有属于自己的内存),只是依附于数组的一些信息,和数组捆绑存储,这样,对数组名取地址得到的自然还是数组的首地址
-
你可以修改指针指向的地址,但你不能修改数组名的指向
数组名退化为指针¶
众所周知,在数组名作为参数传给函数时,数组名会退化为指针。因为用来接收数组名的形参是指针,而指针只能存一个地址,而不能存数组大小等信息,因此会发生信息的丢失,也就是我们说的 数组名退化
void test(int* a){
printf("%#x\n",a); // Result: 0xee01a8d0
printf("%#x\n",&a); // Result: 0xee01a8a8
}
int main(){
int arr[5];
printf("%#x\n",arr); // Result: 0xee01a8d0
printf("%#x\n",&arr); // Result: 0xee01a8d0
test(arr);
return 0;
}
通过这个程序我们可以看到,arr
和 a
都存储了数组的首地址 0xee01a8d0
,而 a
的地址 &a
不同于数组首地址,这说明编辑器新建了一个指针变量,并将数组首地址存进去了,实际上,这一过程就是我们常说的 传值传参
既然问题出在传参上,那我们常用的防止拷贝复制的传参方式 传引用 ,能不能用在这里防止数组名退化呢?1
void test(int (&a)[5]){
printf("%#x\n",a); // Result: 0xee01a8d0
printf("%#x\n",&a); // Result: 0xee01a8d0
printf("%lu\n",sizeof(a)); // Result: 20
}
int main(){
int arr[5];
printf("%#x\n",&arr); // Result: 0xee01a8d0
test(arr);
return 0;
}
可以看到,改用传引用的方式传递数组名后,没有发生数组名退化为指针的情况,对 a
使用 sizeof
运算符也能得到数组所占内存大小 20
但这一方法要求我们在形参中指定传入数组的大小,如果形参指定的大小和传入数组的大小不一样,就会报错
这一问题可以使用模板解决:
template<int T>
void test(int (&a)[T]){
printf("%lu\n",sizeof(a));
for(auto i:a)
printf("%d ",i);
putchar('\n');
}
int main(){
int a[3]={1,2,3};
int b[6]={1,1,4,5,1,4};
test(a);
test(b);
return 0;
}
通过模板,我们引进了一个 int
的 非类型参数(表示一个值而非一个类型),当调用 print
函数时,编译器就会从实参中推断出参数数组的大小 T,并且实例化对应的函数模板
对于二维数组也是类似的,函数声明作如下修改即可:
template<int T>
void test(int (&a)[T][T])
这样我们就可以愉快地在函数里用 sizeof
和 a[i][j]
了!
Question
C 没有模板怎么搞呢?
指针与二维数组¶
二维数组的实质¶
二维数组 int a[n][m]
存的是 \(n\) 个一维数组,所以有:
-
a[i]
可以得到二维数组a[n][m]
中存储的第i
个 一维数组的数组名注意,得到的是 一维数组的数组名 而不是 首地址,也就是说
sizeof(a[i])
会得到m*sizeof(int)
,而不是sizeof(int*)
int a[3][2]={{1,2},{3,4},{5,6}}; printf("%lu\n",sizeof(a)); // Result: 3*2*sizeof(int)=24 printf("%lu\n",sizeof(a[0])); // Result: 2*sizeof(int)=8 printf("%#x %#x\n",a[0],&a[0]); // Result: 0xec720850 0xec720850
可以看到,
a[0]
具有数组名的性质利用这一点,我们可以使用 指针数组 和 指向指针数组的指针(多级指针) 对二维数组进行模拟:
int a[3][2]={1,2,3,4,5,6}; int *p[3]={a[0],a[1],a[2]}; // 指针数组 int **pp=p; // 指向指针数组的指针 (多级指针) // pp[i][j] 与 a[i][j] 完全等效
-
二维数组名的指针运算
作为数组名肯定是不能自增自减的,只能
+i
或-i
“
a[n][m]
存的是 \(n\) 个一维数组 ”,这告诉我们,二维数组中的元素是一维数组,那+1
就应该是 加一个一维数组的大小
手动寻址¶
二维数组在 内存中连续存储,也就是说,二维数组里存的若干个一维数组,在内存中是 首尾相接 的,这是数组名退化为指针后手动寻址的基础
记数组 a[n][m]
的数组名 a
退化为指针 p
,则有:
手动寻址公式:a[i][j]==*(p+n*i+j)
对于二维数组,最方便的传参方式还是上面提到的 传引用方式
const 修饰的指针¶
-
常指针:
const int* p=&a
指向常量的指针,指针所指地址可以变,但所指地址存的东西不能变
-
指针常量:
int* const p=&a
指针类型的常量,指针所指地址不能变,但所指地址存的东西可以变
-
指向常量的指针常量:
const int* const p=&a
所指地址,和所指地址存的东西都不能变
Note
可以这样记忆:const
修饰谁,谁就不能变
const int* p
中,const
修饰了 *p
,所以所指地址存的东西不能变
int* const p
中 const
只修饰了 p
所以是指针所指地址不能变
void 指针¶
即没有指定基类型的指针,可以声明函数返回值和形参为任何类型,这一点常用于 实现泛型
比如 memcpy
可以将一块内存中的内容拷贝到另一块内存中去,很明显,这一过程中我们没必要知道内存中存的数据是什么类型的,所以我们可以使用 void
指针( void* memcpy(void* dest,const void* src,size_t len)
)
Warning
void*
指针的偏移在不同平台上可能有差别,尽量避免使用
如:ANSI C
规定 void*
指针不能偏移;而 GNU
规定 void*
指针的偏移规则与 char*
完全相同
函数指针¶
函数名和数组名非常类似,也不是实体变量,没有分配内存,对其取地址、解引用得到的都是它自己
也就是:function==&function==*function
幸运的是,函数名不像数组名那样复杂,可以近似地认为不管对函数名干什么,得到的都是 函数的入口地址
可以声明一个指向函数的指针(即 函数指针),格式如下:返回值类型 (*指针名)(形参列表)
函数指针常用与传入比较函数,提供比较规则 2
Note
提供比较规则的常用手段还有:重载运算符、声明比较类
bool cmp(const item & x,const item & y){
return x<y; // 从小到大排序
}
bool (*p)(const item&,const item&)=cmp;
std::sort(d.begin(),d.end(),cmp); // cmp 形参就是一个函数指针
cout<<(*p)(3,4)<<endl; // Result: 1