目录
1、指针的本质分析
1、*号的意义
程序中的变量只是一段存储空间的别名, 指针是一种特别的变量, 指针所保存的值是内存的地址, 可以通过指针修改内存中的任意地址内容
在指针声明时,*号表示所声明的变量为指针 ,在指针使用时,*号表示取指针所指向的内存空间中的值
同理:* 的操作数必须是指针,当 * 作用于指针变量时就可以获得指针所指向的单元
实例分析
#include <stdio.h>
int main()
{
int i = 0;
int* pI;
char* pC;
float* pF;
pI = &i; //指针变量pI保存着i的内存地址
*pI = 10; //获得pI保存地址的单元即i赋值10
printf("%p, %p, %d\n", pI, &i, i);
printf("%d, %d, %p\n", sizeof(int*), sizeof(pI), &pI);
printf("%d, %d, %p\n", sizeof(char*), sizeof(pC), &pC);
printf("%d, %d, %p\n", sizeof(float*), sizeof(pF), &pF);
return 0;
}
(32位)
(64位)
2、传值调用与传址调用
指针是变量,因此可以声明指针参数 ,当—个函数体内部需要改变实参的值,则需要使用指针参数,函数调用时实参值将复制到形参
指针适用于复杂数据类型作为参数的函数中
//利用指针交换变量 test.c
#include <stdio.h>
int swap(int* a, int* b)
{
int c = *a;
*a = *b;
*b = c;
}
int main()
{
int aa = 1;
int bb = 2;
printf("aa = %d, bb = %d\n", aa, bb);
swap(&aa, &bb);
printf("aa = %d, bb = %d\n", aa, bb);
return 0;
}
关于传址与传值的效率分析参见另一篇文章:https://blog.csdn.net/qq_39654127/article/details/94713614
3、常量与指针
常量指针(p指向的内容不可变 ):const int* p; int const* p;
指针常量 (p不可变 ):int* const p;
*p与p均为常量(p和p指向的内容都不可变 ):const int* const p;
常量与指针 test.c
#include <stdio.h>
int main()
{
int i = 0;
const int* p1 = &i;
int const* p2 = &i;
int* const p3 = &i;
const int* const p4 = &i;
*p1 = 1; // compile error
p1 = NULL; // ok
*p2 = 2; // compile error
p2 = NULL; // ok
*p3 = 3; // ok
p3 = NULL; // compile error
*p4 = 4; // compile error
p4 = NULL; // compile error
return 0;
}
2、数组的本质分析
1、数组的基本概念
数组是相同类型的变量的有序集合 ,int a[5]代表数组包含5个int类型的数据,a代表数组第一个元素的地址,a也代表这20个字节内存的名字
数组在—片连续的内存空间中存储元素 ,数组元素的个数可以显示或隐式指定 , C语言中的数组大小必须在编译期就作为常数确定
数组名代表数组首元素的地址 ,数组的地址需要用取地址符&才能得到 ,数组首元素的地址值与数组的地址值相同 ,但两者是两个不同的概念
#include <stdio.h>
int main()
{
int a[5] = {1, 2}; //显式指定 a[2], a[3], a[4]的值是0
int b[] = {1, 2}; //隐式指定 b包含了2个元素
printf("a[2] = %d\n", a[2]); //0 即未初始化部分为0
printf("a[3] = %d\n", a[3]); //0
printf("a[4] = %d\n", a[4]); //0
printf("sizeof(a) = %d\n", sizeof(a)); // 4 * 5 = 20
printf("sizeof(b) = %d\n", sizeof(b)); // 4 * 2 = 8
printf("count for a: %d\n", sizeof(a)/sizeof(int)); // 20 / 5 = 5
printf("count for b: %d\n", sizeof(b)/sizeof(int)); // 8 / 4 = 2
printf("a = %p\n", a); // 数组首元素地址
printf("&a = %p\n", &a); // 数组的地址 如果 +1 = ?
printf("&a[0] = %p\n", &a[0]); // 数组首元素地址
return 0;
}
数组名可以看做—个指针常量(int* const p),数组名“指向”的是内存中数组首元素的起始位置 ,数组名不包含数组的长度信息
在表达式中数组名只能作为右值使用 ,只有在下列场合中数组名不能看做指针常量
-数组名作为sizeof操作符的参数 ,代表计算a代表的数组内存大小
-数组名作为&运算符的参数,代表取数组的地址
#include <stdio.h>
int main()
{
int a[5] = {0};
int b[2];
int* p = NULL;
p = a;
printf("a = %p\n", a);
printf("p = %p\n", p);
printf("&p = %p\n", &p);
printf("sizeof(a) = %d\n", sizeof(a)); //20
printf("sizeof(p) = %d\n", sizeof(p)); //4
printf("\n");
p = b;
printf("b = %p\n", b);
printf("p = %p\n", p);
printf("&p = %p\n", &p);
printf("sizeof(b) = %d\n", sizeof(b)); //8
printf("sizeof(p) = %d\n", sizeof(p)); //4
//b = a; //error: assignment to expression with array type
return 0;
}
数组名绝对不等于指针,且不是指针,只是很多情况可以看成指针常量处理
2、数组的底层分析
VC++6.0
通过一个基址即可定位所有元素,不论一维还是多维在底层都是一维的
数组的内存分配原理
①空main函数时
②只定义一个int a[5]局部数组时
③只定义一个char a[5]局部数组时
实际多分配了3字节
下面看看一个有趣的程序
#include <stdio.h>
#include <Windows.h>
void print()
{
while (1)
{
printf("Hello!!!\n");
Sleep(1000);
}
}
void check()
{
int a[10];
a[11] = (int)&print; //越界访问!!!
}
int main()
{
check();
return 0;
}
执行结果:
分析原因:
3、指针和数组分析
1、指针的运算、a与&a
指针是一种特殊的变量,与整数的运算规则为 : p + n; ↔ (unsigned int)p + n * sizeof( *p );
当指针p指向—个同类型的数组的元素时: p + 1 将指向当前元素的下—个元素; p - 1将指向当前元素的上—个元素。
指针之间只支持减法运算 , 参与减法运算的指针类型必须相同 , 运算规则为 : p1 - p2; ↔ ( (unsigned int)p1 - (unsigned int)p2 ) / sizeof(type);
注意:
1. 只有当两个指针指向同—个数组中的元素时,指针相减才减有意义,其意义为指针所指元素的下标差
2. 当两个指针指向的元素不在同一个数组中时,结果未定义
3. 指针也可以进行关系运算(<, <=,>,>=) ,前提是同时指向同—个数组中的元素
4. 任意两个指针之间的比较运算( ==,!= )无限制 ,参与比较运算的指针类型必须相同
#include <stdio.h>
int main()
{
int a[5] = {0};
int* p = NULL;
printf("a = 0x%X\n", (unsigned int)(a));
printf("a + 1 = 0x%X\n", (unsigned int)(a + 1));
printf("p = 0x%X\n", (unsigned int)(p));
printf("p + 1 = 0x%X\n", (unsigned int)(p + 1));
return 0;
}
a为数组首元素的地址 ,&a为整个数组的地址
#include <stdio.h>
int main()
{
int a[5] = {1, 2, 3, 4, 5};
int* p1 = (int*)(&a + 1); //指向5后面一个位置
int* p2 = (int*)((int)a + 1);
int* p3 = (int*)(a + 1);
printf("%d, %d, %d\n", p1[-1], p2[0], p3[1]);
return 0;
}
gcc32位
#include <stdio.h>
int main()
{
char s1[] = {'H', 'e', 'l', 'l', 'o'};
int i = 0;
char s2[] = {'W', 'o', 'r', 'l', 'd'};
char* p0 = s1; //指向数组第一个元素
char* p1 = &s1[3];
char* p2 = s2;
int* p = &i;
printf("%d\n", p0 - p1); // -3
//printf("%d\n", p0 + p2); //error
printf("%d\n", p0 - p2); //虽然编译通过,但结果不可预期 -5
//printf("%d\n", p0 - p); //error
//printf("%d\n", p0 * p2); //error
//printf("%d\n", p0 / p2); //error
return 0;
}
#include <stdio.h>
#define DIM(a) (sizeof(a) / sizeof(*a))
int main()
{
char s[] = {'H', 'e', 'l', 'l', 'o'};
char* pBegin = s;
char* pEnd = s + DIM(s); // Key point
char* p = NULL;
printf("pBegin = %p\n", pBegin);
printf("pEnd = %p\n", pEnd);
printf("Size: %d\n", pEnd - pBegin);
for(p=pBegin; p<pEnd; p++)
{
printf("%c", *p);
}
printf("\n");
return 0;
}
0xbf9e07b7 + 5 * sizeof(char) = 0xbf9e07bc
2、数组的访问方式
数组名可以当作指针常量使用, 指针也可以当作数组名来使用
- 以下标的形式访问数组中的元素 a[0],a[1]
- 以指针的形式访问数组中的元素 *(a+0),*(a+1)
下标形式与指针形式的转换 : a[n] ⇿ *(a + n) ⇿ *(n + a) ⇿ n[a]
指针以固定增量在数组中移动时,效率高于下标形式 ,指针增量为1且硬件具有硬件增量模型时,效率更高
现代编译器的生成代码优化率已大大提高,在固定增量时,下标形式的效率已经和指针形式相当;但从可读性和代码维护的角度来看,下标形式更优
#include <stdio.h>
int main()
{
int a[5] = {0};
int* p = a;
int i = 0;
for(i=0; i<5; i++)
{
p[i] = i + 1; //将指针当作数组名使用
}
for(i=0; i<5; i++)
{
printf("a[%d] = %d\n", i, *(a + i));
}
printf("\n");
for(i=0; i<5; i++)
{
i[a] = i + 10;
}
for(i=0; i<5; i++)
{
printf("p[%d] = %d\n", i, p[i]);
}
return 0;
}
虽然指针也可以当作数组名来使用,但数组和指针不同
ext.c
int a[] = {1, 2, 3, 4, 5}; // 定义数组
29-2.c
#include <stdio.h>
int main()
{
extern int a[]; // 声明已经定义了数组a
printf("&a = %p\n", &a);
printf("a = %p\n", a);
printf("*a = %d\n", *a);
return 0;
}
此时打印和预期结果一样,当修改为extern int* a; 即告诉编译器a是个指针变量(可能会报重定义错误int[5], int* 冲突)
a是指针变量,打印值,会在&a里取4个字节(即第一个元素)访问0x1地址值肯定会错误
3、虚幻的数组参数
数组作为函数参数时,编译器将其编译成对应的指针
一般情况下,当定义的函数中有数组参数时,需要定义另—个参数来标示数组的大小
#include <stdio.h>
void func1(char a[5])
{
printf("In func1: sizeof(a) = %d\n", sizeof(a)); //4
*a = 'a'; //确定是否是指针
a = NULL; //确定是否是数组名(数组名不能作左值)
}
void func2(char b[])
{
printf("In func2: sizeof(b) = %d\n", sizeof(b)); //4
*b = 'b';
b = NULL;
}
int main()
{
char array[10] = {0};
func1(array);
printf("array[0] = %c\n", array[0]);
func2(array);
printf("array[0] = %c\n", array[0]);
return 0;
}
4、二级指针与二维数组
1、分析二级指针与二维数组
指针的本质是变量 ,可以定义指向指针的指针来保存指针变量的地址值
int main()
{
int i = 0;
int* p = NULL; // 定义指针变量p
int** pp = NULL; // 定义二级指针变量pp
pp = &p; // 二级指针变量pp保存指针变量p的地址
*pp = &i; // 指针变量p保存变量i的地址
return 0;
}
指针在本质上也是变量,对于指针也同样存在传值调用与传址调用
#include <stdio.h>
#include <malloc.h>
// 重置动态空间大小
int reset(char**p, int size, int new_size)
{
int ret = 1;
int i = 0;
int len = 0;
char* pt = NULL;
char* tmp = NULL;
char* pp = *p; // *p --> main中的p
if( (p != NULL) && (new_size > 0) )
{
pt = (char*)malloc(new_size);
tmp = pt;
len = (size < new_size) ? size : new_size;
for(i=0; i<len; i++)
{
*tmp++ = *pp++; //将原来动态申请的空间的值放到新申请的空间
}
free(*p); //释放原来申请的空间
*p = pt; //指针指向新的内存空间
}
else
{
ret = 0;
}
return ret;
}
int main()
{
char* p = (char*)malloc(5);
printf("%p\n", p);
if( reset(&p, 5, 3) ) //传址调用
{
printf("%p\n", p);
}
free(p);
return 0;
}
需要通过reset返回修改指针变量p的值,所以实参为指针的地址,形参为二级指针
二维数组在内存中以一维的方式排布 ,二维数组中的第一维是一维数组( int [3] ) ,二维数组中的第二维才是具体的值 ,二维数组的数组名可看做指针常量
遍历二维数组
#include <stdio.h>
#include <malloc.h>
void printArray(int a[], int size) //线性访问
{
int i = 0;
printf("printArray: %d\n", sizeof(a));
for(i=0; i<size; i++)
{
printf("%d\n", a[i]);
}
}
int main()
{
int a[3][3] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}};
int* p = &a[0][0];
int i = 0;
int j = 0;
for(i=0; i<3; i++)
{
for(j=0; j<3; j++)
{
printf("%d, ", *(*(a+i) + j)); // *(a+i)=>a[i], *(a[i]+j)=>a[i][j] ps: a[i]的类型为int[3]
}
printf("\n");
}
printf("\n");
printArray(p, 9);
return 0;
}
—维数组名代表数组首元素的地址
int a[5] a的类型为int*
二维数组名同样代表数组首元素的地址
int a[2][5] a的类型为int(*) [5]
(两个元素a[0], a[1], 每个元素的类型是int[5], 即每个元素都是一个一维数组,a[0],a[1]是数组名,&a[0]的类型就是int (*) [5])
结论:
1. 二维数组名可以看做是指向数组的指针常量
2. 二维数组可以看做是—维数组 ,二维数组中的每个元素都是同类型的一维数组
#include <stdio.h>
int main()
{
int arr[3][3] =
{
{1,2,3},
{4,5,6},
{7,8,9}
};
printf("sizeof (arr[0]) = %d\n", sizeof (arr[0])); // 12
int (*p) [3] = &arr[0]; //arr[0]为一维数组int[3]的数组名,访问某个元素a[0][j], &arr[0]类型为int (*) [3] 数组指针,arr为数组首元素arr[0]的地址
printf("arr[0][0] = %d\n", (*p)[0]); //1
p++;
printf("arr[1][0] = %d\n", (*p)[0]); //4
printf("*(arr + 1) = %d\n", **(arr + 1)); //4 arr + 1 -> &arr[0] + 1 ->&arr[1]
int* p1[3];
p1[0] = arr[1]; // 指向一维数组的第二个元素(相当于int a[3]; int* p = a)
printf("*(p[0])) = %d\n", *(p1[0])); //4
printf("*(p[0]+1)) = %d\n", *(p1[0]+1)); //5
return 0;
}
#include <stdio.h>
#include <malloc.h>
// 动态申请二维数组
int** malloc2d(int row, int col)
{
int** ret = NULL;
if( (row > 0) && (col > 0) )
{
int* p = NULL;
ret = (int**)malloc(row * sizeof(int*));
p = (int*)malloc(row * col * sizeof(int));
if( (ret != NULL) && (p != NULL) )
{
int i = 0;
for(i=0; i<row; i++)
{
ret[i] = p + i * col; //一维数组的每个元素指向一个一维数组
}
}
else
{
free(ret);
free(p);
ret = NULL;
}
}
return ret;
}
void free2d(int** p)
{
if( *p != NULL )
{
free(*p);
}
free(p);
}
int main()
{
int** a = malloc2d(3, 3);
int i = 0;
int j = 0;
for(i=0; i<3; i++)
{
for(j=0; j<3; j++)
{
printf("%d, ", a[i][j]);
}
printf("\n");
}
free2d(a);
return 0;
}
2、数组参数和指针参数
C语言中只会以值拷贝的方式传递参数
C语言以高效作为最初设计目标: 参数传递的时候如果拷贝整个数组执行效率将大大下降。 参数位于栈上,太大的数组拷贝将导致栈溢出。
C语言中的数组参数必然退化为指针
二维数组参数中第—维的参数可以省略
float* a指向的一维数组的每个元素为float型
int** a指向的一维数组的每个元素为int*类型
char(*a)[4]指向的一维数组的每个元素为int[4]类型
C语言中无法向—个函数传递任意的多维数组 ,必须提供除第—维之外的所有维长度
第一维之外的维度信息用于完成指针运算 , N维数组的本质是一维数组,元素是N-1维的数组 ,对于多维数组的函数参数只有第—维是可变的
传递与访问二维数组
#include <stdio.h>
void access(int a[][3], int row) //int (*a)[3]
{
int col = sizeof(*a) / sizeof(int);
int i = 0;
int j = 0;
printf("sizeof(a) = %d\n", sizeof(a)); //4
printf("sizeof(*a) = %d\n", sizeof(*a)); //12
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d\n", a[i][j]);
}
}
printf("\n");
}
void access_ex(int b[][2][3], int n) //int (*a)[2][3]
{
int i = 0;
int j = 0;
int k = 0;
printf("sizeof(b) = %d\n", sizeof(b)); //4
printf("sizeof(*b) = %d\n", sizeof(*b)); //24
for(i=0; i<n; i++)
{
for(j=0; j<2; j++)
{
for(k=0; k<3; k++)
{
printf("%d\n", b[i][j][k]);
}
}
}
printf("\n");
}
int main()
{
int a[3][3] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}};
int aa[2][2] = {0};
int b[1][2][3] = {0};
access(a, 3);
// access(aa, 2); /*expected'int (*)[3]', but argument is type of 'int (*)[2]'*/
access_ex(b, 1);
// access_ex(aa, 2); /*expected'int (*)[2][3]', but argument is type of 'int (*)[2]'*/
return 0;
}