本章主要讲解指针的基本定义和指针的传递、偏移。后面继续讲解指针数组和多维指针、二级指针等
知识点:
使用指针的需求 将某地址保存下来
指针使用的场景 传递与偏移
在 C 语言中,指针是一种特殊的变量,它存储的是另一个变量的内存地址。您可以使用指针来访问和修改另一个变量的值。
要声明一个指针,需要在变量类型前面加上一个星号 *
。例如,下面是声明一个整型指针的示例:
int *ptr;
这会声明一个名为 ptr 的指针,它存储的是一个整型变量的地址。
要为指针赋值,可以使用一个变量的地址运算符 &。例如,下面是为指针赋值的示例:
int x = 10;
int *ptr;
ptr = &x;
这会将 ptr 指向变量 x,因此您可以使用指针访问变量 x 的值。
要使用指针访问变量的值,可以使用指针值运算符(取值运算符) *。例如,下面是使用指针访问变量的示例:
int x = 10;
int *ptr;
ptr = &x;
printf("%d\n", *ptr); // 输出 10
这会输出变量 x 的值,即 10。
请注意,您必须在使用指针之前给它赋值。如果尝试使用未初始化的指针,可能会出现未定义的行为。
警惕野指针:定义未初始化JNULL,赋值。结束释放空间未定义NULL。
指针在32位系统:4字节
指针在64位系统:8字节
一般都是32位4字节
值传递
值传递是指在函数调用过程中,将函数外部的变量的值复制给函数内部的形参变量,在函数内部对形参变量的操作并不会影响到函数外部的变量。
举个例子:
def add_one(x):
x += 1
return x
a = 5
b = add_one(a)
print(a) # 输出 5
print(b) # 输出 6
在这个例子中,我们定义了一个函数 add_one,该函数接受一个参数 x,并将 x 加上 1 后返回。
我们在函数外部定义了一个变量 a,并将其作为参数传递给函数 add_one。
在函数内部,我们将 x 加上 1,但这并不会影响到函数外部的变量 a。
所以,当我们调用 print(a) 时,会输出 5。同时,由于函数 add_one 返回了 x 加上 1 后的值,所以调用 print(b) 时会输出 6。
这就是值传递的基本原理: 在值传递的过程中,函数内部的形参变量是与函数外部的实参变量隔离开来的,对形参变量的操作不会影响到实参变量
。
值传递不会改变实参的值,二者是分开的,那么他们的内存地址一样不?
在调用函数时,实参和形参都会占用内存空间。这意味着实参和形参在内存中都有相应的地址。
形参是函数定义中声明的变量,它们在函数调用时才会被分配内存。
实参是函数调用时传递给函数的变量,它们在程序执行期间就已经分配了内存。
尽管实参和形参都占用内存,但是它们的 内存地址是不同的
。形参的内存地址是在函数调用时才分配的,而实参的内存地址在程序执行期间就已经分配了。
在值传递过程中,实参和形参位于内存中两个不同地址中,实参先自己复制一次拷贝,再把拷贝复制给形参。所以,在值传递过程中,形参的变化不会对实参有任何的影响。
例如:
数组作为实参的时候,这时候实参和形参都是指针,这两个指针是分开存储的,测试代码如下:
#include
void change(int a[]) {
a++;
printf("%d\n",a[0]);
}
int main() {
int a[]={1,2};
change(a);
printf("%d\n",a[0]);
}
运行结果是2 1, 所以是没问题的.存储地址不相同。
这里注意下指针的情况:
无论是不是指针,形参实参都不是占用相同的空间。
不是指针时,形参和实参的值是相等的;
当是指针时,形参和实参都指向同一个地址(其实也就是* p(形参)和*q(实参)的值是相等的),但绝不是相同存储空间
也就是说 使用指针时候指向的地址一样(值一样),但是本身的指针的存储空间肯定不一样。
如果还不明白下面咱们看看值传递的内部过程吧!
举例:
//change函数
void change(int j){
j=5;}
int main(){
int i=10;
printf("before change i=%d\n" ,i);
change(i); //调用change函数,改变值,但是void函数,形参不会改变实参值
printf("after change i=%d\n",);
system(" pause");
return 0;
}
监视窗口中输入&i,可以看到变量i的地址是0x0023F858。按F11 键进人change函数,这时变量j的值的确为10,但是&j的值为0x0023F784,也就是j和i的地址并不相同。
这一步证明了实参形参使用的地址确实不相同。
运行j=5后,change函数实际修改的是地址0x0023F784上(j)的值,从10变成了5,接着change函数执行结束,变量i的值肯定不会发生改变,因为变量i的地址是0x0023F858而非0x0023F784. 然后他没有返回值,所以不会对i的值造成影响。
程序的执行过程其实就是内存的变化过程,我们需要关注的是 栈空间的变化。
当main函数开始执行时,系统会为main函数开辟函数栈空间
当程序走到inti时,main函数的栈空间就会为变量i分配4字节大小的空间。
调用change函数时,系统会为change函数重新分配新的函数栈空间,并为形参变量j分配4字节大小的空间。
在调用change(i)时,实际上是将i的值赋值给j,我们把这种效果称为值传递(C 语言的函数调用称为值传递)。
因此,当我们在change函数的函数栈空间内修改变量j的值后,change函数执行结束,其栈空间就会释放,j 就不再存在,i 的值不会改变。
引用传递&
等于把实参的地址传递过去,形参复制实参地址,改变内容影响实参
在引用传递中,函数调用时传递的是参数的地址。这意味着,在函数内部对参数的更改会影响到函数调用时传递的参数的值。
可以使用指针实现
#include
#include
void change(int* j){
*j=5; //间接 访问得到变量i
}
//指针的传递
int main(){
int i=10;
printf("before change i=%d\n" ,i);
change(&i); //传 递变量i的地址
printf("after change i=%d\n" ,);
system(" pause");
return 0;}
我们可以看到程序执行后,语句printf(" after change i=%d\n",);打印的i的值为5
不相同,只是把指针传递过去了。变量本身的地址肯定不一样。本质上还是值传递,不过传递的现在是指针了。
例如,下面的代码定义了一个函数 swap,用来交换两个整数的值:
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 1, y = 2;
swap(&x, &y);
printf("x = %d, y = %d\n", x, y); // x = 2, y = 1
return 0;
}
在这个例子中,swap 函数的形参是两个指针变量 a 和 b,实参是两个整数变量 x 和 y 的地址。在函数内部,通过使用指针 a 和 b 访问实参的值,就可以实现交换 x 和 y 的值的目的。
那么变量a和b的地址与 x y的地址相同吗
在上面的代码中,变量 a 和 b 是形参,是在函数内部声明的指针变量。变量 x 和 y 是实参,是在 main 函数中声明的整数变量。
在调用 swap 函数时,会将 x 和 y 的地址传递给 a 和 b。这意味着,在函数内部,变量 a 和 b 的值是 x 和 y 的地址。 但是,变量 a 和 b 的地址并不是 x 和 y 的地址。每个变量都有自己的地址,不同的变量地址是不同的。
例如,在上面的代码中,可以使用 & 运算符来获取变量的地址。所以,如果你想要获取变量 a 的地址,可以使用 &a,如果想要获取变量 x 的地址,可以使用 &x。
总的来说,在 C 语言中,函数的实参和形参的地址并不相同,但是可以通过使用指针的方式,让函数访问并修改实参的值。
补充,在c++中,我记得直接在函数形参前面加一个&就行了,表示引用符号
前面介绍了指针的传递。指针即地址,就像我们找到了一栋楼,这栋楼的楼号是B,那么往
前就是A,往后就是C,所以应用指针的另一个场景就是对其进行加减,但对指针进行乘除是没有意义的,就像家庭地址乘以5没有意义那样。
指针的加减称为指针的偏移
举例:
int a[5]={1,2,3,4,5};
int *p;
直接用a代表a[5]数组的起始地址,也就是a[0]的地址。
运用,这种指针偏移技术可以巧妙的输出数组的元素
也就是说可以加减来获取后面前面元素的地址。
int a[5]={1,2,3,4,5};
int *p; //对一个整形遍历进行取值
p=a; //p指针指向 数组a起始地址
prinf("%d\n",*p);
for(int i=0;i<5;i++)
{
printf("%d\n",*(p+i));
}
return 0;
这里使用了p+1 这种类型的偏移加,来实现遍历数组元素。
以上面为例:
假设数组名中存储着数组的起始地址0x28F768,其类型为整型指针,所以可以将其赋值给整型指针变量p,可以从监视窗口中看到p+1的值为0x28F76C.
因为指针变量加1后,偏移的长度是其基类型的长度,也就是偏移sizeof(int)
,这样通过 *(p+1)就可以得到元素a[1].
编译器在编译时,数组取下标的操作正是转换为指针偏移来完成的。
既然掌握了指针的使用场景,那么为什么还要了解指针与自增、自减运算符的关系呢?其实,
这就像我们掌握了乘法口诀,但是仍然要做各种乘法运算题一一样。通过一些训练,可以避免在使用中发生错误。
#include
#include
//只有比后增优先级高的操作符,才会作为一个整体, 如()、 []
int main()
{
int a[3]={2,7,8};
int *p;
int j;
p=a; //指针指向数组首地址
j=*p++; //先把*p 的值2赋给j,然后对p加1
print("a[O]=%d,j=%d,*p=%d\n" a[O],j,*p);
j=p[0]++; //先把 p[0]赋给j,然后对p[0]加1
print("a[0]=%d,j=%d,*p=%d\n" ,a[]j,*p);
system(" pause");
return 0;
}
还是按照前缀自增自减和后缀自增自减的规则,按优先级,同级按结合顺序。
为什么第-次输出的是j=2,* p=7呢?
首先,前面讲过当遇到后增操作符时,就要分两步来看。
这里实际上是对p进行++操作,因为*操作符和++操作符的优先级相同(结合顺序是右往左),只有比++优先级高的操作符才会当成一个整体,目前我们用过的比++操作符优先级高的只有()和[]两个操作符.
指针数组是一种特殊的数组,其中的每个元素都是一个指针。这意味着,指针数组中的每个元素都指向一个变量或内存位置。
在 C 和 C++ 中,指针数组可以使用以下语法声明:
type *arrayName[size];
其中,type 是指针指向的变量的数据类型,arrayName 是指针数组的名称,size 是数组的大小。例如,下面是声明一个指向整数的指针数组的示例:
int *ptrArray[10];
在这种情况下,ptrArray 是一个指向整数的指针的数组,它有 10 个元素。您可以使用下标来访问数组中的元素,例如,ptrArray[0] 是指针数组中第一个元素的指针。
您可以使用指针数组来存储多个指针,并使用它们来存储多个变量的地址。例如,您可以使用指针数组来存储多个字符串的地址,然后使用指针数组中的元素来访问这些字符串:
char *strArray[10];
strArray[0] = "Hello";
strArray[1] = "World";
printf("%s %s\n", strArray[0], strArray[1]); // prints "Hello World"
指针与一维数组:可以取下标来改变数组元素。
数组名作为实参传递给子函数时,是弱化为指针的
void change(char *d)
{
*d='H';
d[1]='E';
*(d+2)='L';
}
int main(){
char c[10]='hello';
change(c);
puts(c);
}
这里把数组c通过指针传递首地址给*d ,d指针。然后d就相当于现在是从c的首地址开始的,然后可以通过改变下标【】遍历,也可以使用指针偏移(d+1)来遍历。
很多读者在学习C语言的数组后都会觉得数组长度固定很不方便,其实C语言的数组长度
固定是因为其定义的整型、浮点型、字符型变量、数组变量都在栈空间中,而栈空间
的大小在编译时是确定的。如果使用的空间大小不确定,那么就要使用堆空间
。
栈空间是一种特殊的内存区域,它由计算机系统自动分配并管理。它用于存储程序运行时创建的临时数据,如函数调用时传递的参数和局部变量等。栈空间的特点是先进后出,也就是说,新的数据会被放在栈顶,而当程序结束时,最后一个被放入栈中的数据会最先被弹出。
堆空间是一种通用的内存区域,它由程序员手动分配并管理。它用于存储程序运行期间创建的动态对象,如使用 new 运算符创建的对象等。堆空间的特点是随机存取,也就是说,程序可以随时在堆中分配内存,并在需要时释放内存。
总的来说,栈空间更快速,但容量较小,而堆空间容量较大,但访问速度较慢。程序员需要根据实际需要合理使用这两种内存区域。
栈是计算机系统提供的数据结构,计算机会在底层对栈提供支持:
#include
#include
#include
int main()
{
int i;
char *p;
scanf("%d",&i); //输入要申请的空间大小
p=(char*)malloc(i); // 使用malloc动态申请堆空间
strcpy(p,"malloc success");
puts(p);
free(p); //free 时必须使用malloc申请时返回的指针值,不能进行任何偏移
printf("free success\n");
system("pause");
}
这就是一个正常常规的malloc申请内存空间。
首先我们来看malloc函数。
#include void *malloc(size_ t size);
需要给malloc传递的参数是一个整型变量,所以这里的size_ t 即为int
; 返回值为void*类型的指针
,void*类型
的指针只能用来存储一个地址而不能进行偏移,因为malloc并不知道我们申请的空间用来存放什么类型的数据,所以确定要用来存储什么类型的数据后,都会将void*强制转换为对应的类型
。
所以写的形式是
char *p; p=(char*)malloc(i); // 使用malloc动态申请堆空间
这里注意下malloc申请内存的空间是字节
单位,也就是这里申请一个char 型需要1个字节。(char *)mallloc(1);
这里就延伸了使用sizeof()来求长度。可以套用里面。
如图所示,定义的整型变量i、指针变量p均在main函数的栈空间中,通过malloc
申请的空间会返回一个堆空间的首地址
,我们把首地址存入变量p。知道了首地址,就可以通过strcpy函数往对应的空间存储字符数据。
解析 malloc 在 # include< stdlib. h> 头文件中, 函数的定义为void* malloc(size_ t size) void * 表示定义的为无类型指针,因为是无类型所以才使用强制类型转换,在前面加上 (char *) , malloc 申请空间的单位是字节
栈空间由系统自动管理,而堆空间的申请和释放需要自行管理,所以在具体例子中需要通过free函数释放堆空间。free 函数的头文件及格式为
#include
不需要强制类型转换
。上面例子是:free(p); //free 时必须使用malloc申请时返回的指针值,不能进行任何偏移
p的地址值必须是malloc当时返回的地址值,不能进行偏移,也就是在malloc和free之间不能进行p++等改变变量p的操作
原因是: 会匹配不上首地址
申请段堆内存空间时,内核帮我们记录的是起始地址和大小,所以释放时内核用对应的首地址进行匹配,匹配不上时,进程就会崩溃。
比如,你可以定义一个指针变量ptr来指向内存块的首地址,然后定义另一个指针变量offset来记录偏移量。你可以使用这个偏移量来访问内存块中的某个特定位置,比如:
int *ptr = malloc(sizeof(int) * 10); // 申请内存块
int *offset = ptr + 5; // 偏移 5 个 int 大小的位置
*offset = 123; // 将 123 赋值给偏移位置的内存
当然,你也可以使用指针运算符来实现偏移,比如:
int *ptr = malloc(sizeof(int) * 10); // 申请内存块
int *offset = ptr;
offset += 5; // 偏移 5 个 int 大小的位置
*offset = 123; // 将 123 赋值给偏移位置的内存
在使用完内存块后,你需要记得使用free()函数来释放内存。内核会使用你传入的首地址来匹配之前申请的内存块,然后将其释放。所以,如果你使用了偏移量来访问内存块中的某个位置,你需要记得在调用free()时传入内存块的首地址,而不是偏移后的地址。
问题引入:
#include
char* print_stack() {
char c[17] = "i am ok";
puts(c);
return c;
}
int main() {
char* p;
p = print_stack();
puts(p);
return 0;
}
执行效果:
解决方法:
#include
//函数栈空间释放后,函数内的所有局部变量消失
char* print_stack() {
char c[17] = "i am ok";
puts(c);
return c;
}
//堆空间不会因函数执行结束而释放
char* print_malloc(){
char* p=(char*)malloc(30);
strcpy(p,"study study");
puts(p);
return p;
}
int main() {
char* p;
//p = print_stack();
//puts(p);
p=print_malloc();
puts(p);
free(p);
return 0;
}
执行效果:
char *p="hello"
和 char c[10]="hello"
有什么区别呢?
char *p = "hello"; //把字符串型常量"hello"的首地址赋给p
char c[10] = "hello"; //等价于strcpy(c,"hello");
c[0] = 'H';
printf("c[0]=%c\n",c[0]);
printf("p[0]=%c\n", p[0]);
//p[0]='H'; //不能对常量区数据进行修改
p = "world"; //将字符串world的地址赋给p
//c="world" ;//非法
system("pause"); // 防止运行后自动退出,需头文件stdlib.h
return 0;
}
//p[0]='H'; //不能对常量区数据进行修改
会发生运行异常。
常量的定义就是不能被修改的数
指针变量p指向的是一个常量数据。不能修改里面的内容。
对 p[0]
进行修改,会报错误,然而 c[0]
对数组进行修改可以,因为char c [10] = "hello"
实际等价于 strcpy(c,"hello") ;
操作的是堆区(可读可写),p[0]
实际操作的是字符串常量区(数据区),该区域只读不能写
char *p = "hello"; //把字符串型常量"hello"的首地址赋给p
p = "world"; //将字符串world的地址赋给p
p是一个指针变量,因此我们可以将字符串"world"的首地址重新赋给p
char c[10]="hello"; //这时候c已经是字符串hello的首地址开始了
c="world"; //非法
而数组名c本身存储的就是数组的首地址,是确定的、不可修改的,c 等价于符号常量.因此,如果c=“world”,那么就会造成编译不通。
"c"是一个字符数组,它的值是"hello"的一个副本,存储在内存中的某个位置。您无法将字符数组赋值给另一个值,因此语句"c = “world”"是错误的。
无法把字符数组重新赋其实地址,改变值。
在这种情况下,您可以使用字符数组的索引来修改字符数组中的每个字符。例如,要将"hello"替换为"world",您可以使用以下语句:
c[0] = 'w';
c[1] = 'o';
c[2] = 'r';
c[3] = 'l';
c[4] = 'd';
但是,这种方法有一个缺点,即如果您想要替换的字符串超过字符数组的大小,则可能会发生溢出。
为了避免这种情况,可以使用标准库函数strcpy来复制字符串,例如:
strcpy(c, "world");
这样就可以将"world"复制到字符数组"c"中,而不会发生溢出。
在C语言中,字符数组是一种数据类型,用于存储一个字符串。它包含一个连续的字符序列,并且可以使用索引访问每个字符。例如,要访问字符数组"c"中的第一个字符,可以使用语句"c[0]"。
在C语言中,数组是一种引用类型,因此您 可以通过索引来修改数组中的每个元素
。例如,要将字符数组"c"的第一个字符替换为"w",可以使用语句"c[0] = ‘w’“。这会将字符数组"c"中的第一个字符替换为"w”。
这是因为,当您访问数组中的元素时,实际上是访问数组中对应位置的内存单元。因此,当您使用索引访问字符数组中的元素时,实际上是访问内存中的某个单元,并可以直接修改它的值。
另一方面,字符数组的值是字符串的一个副本,存储在内存中的某个位置。您无法直接将字符数组赋值给另一个值,因此语句"c = “world”"是错误的。要修改字符数组的值,必须使用索引或标准库函数strcpy。
二次指针是指指向指针的指针
,也就是说,二次指针是一种指针,它指向的是另一个指针。
举个例子,假设我们有一个指针 ptr,它指向一个整型变量 x。我们可以定义一个二次指针 ptr2 来指向 ptr,如下所示:
int x = 5;
int *ptr = &x;
int **ptr2 = &ptr;
在上面的例子中,ptr 是一个指针,它指向变量 x。ptr2 是一个二次指针,它指向指针 ptr。
二次指针常用于函数参数传递,例如在函数中修改传入的指针的值。还可以用于动态内存分配,例如使用二次指针来分配一个二维数组。
例如:你可以使用二次指针来分配并初始化一个二维数组。例如,假设你想要分配一个大小为 m 行 n 列的二维数组,并将所有元素初始化为 0,你可以使用如下代码:
#include
#include
int main() {
int m = 3;
int n = 4;
int **a;
// 为二维数组分配内存
a = (int **)malloc(m * sizeof(int *));
for (int i = 0; i < m; i++) {
a[i] = (int *)malloc(n * sizeof(int));
}
// 初始化二维数组
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
a[i][j] = 0;
}
}
// 输出二维数组的内容
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
printf("%d ", a[i][j]);
}
printf("\n");
}
// 释放二维数组的内存
for (int i = 0; i < m; i++) {
free(a[i]);
}
free(a);
return 0;
}
这段代码首先使用 malloc 函数为二维数组分配内存,然后使用两层循环将所有元素初始化为 0。最后,使用另一个两层循环输出二维数组的内容,最后使用 free 函数释放二维数组的内存。
二级指针只服务于一级指针的传递与偏移
要想在子函数中改变一个变量的值,必须把该变量的地址传进去
要想在子函数中改变一个指针变量的值,必须把该指针变量的地址传进去
二级指针的传递
#include
#include
void change(int** p, int* pj) {
int i = 5;
*p = pj;
}
int main() {
int i = 10;
int j = 5;
int* pi;
int* pj;
pi = &i;
pj = &j;
printf("i=%d,*pi=%d,*pj=%d\n", i, *pi, *pj);
change(&pi, pj);
printf("after change i=%d,*pi=%d,*pj=%d\n", i, *pi, *pj);
system("pause");
return 0;
}
整型指针pi指向整型变量i,整型指针pj指向整型变量j.
通过子函数change,我们想改变指针变量pi的值,让其指向j。
由于C语言的函数调用是值传递,因此要想在change中改变变量pi的值,就必须把pi的地址传递给change.
pi是一"级指针,&pi的类型即为二级指针,左键将其拖至内存区域可以看到指针变量pi本身的地址为0x0031F800,对应存储的地址是标注位置1的整型变量i的地址值(因为是小端,所以低位在前)。
接着将其传入函数change,change函数的形参p必须定义为二级指针,然后在change函数内对p进行解引父用,就可以得到pi,进而对其存储的地址值进行改变。
也就是使用change(&pi, pj);
把pi一级指针地址传递给形参** p 二级指针。
然后使用*p = pj;
把 pi一级指针地址值修改成pj指针的值。