本章主要讲解指针的基本定义和指针的传递、偏移。后面继续讲解指针数组和二级指针等
知识点:
- 指针的定义和指针分类
- 各类指针的字节长度取决于系统位数
- 指针的传递(值传递和引用(地址传递))
- 指针的偏移(自增自减号)
- 易错点:
- 指针数组的应用
- 指针动态内存申请
- 字符指针和字符数组的初始化和易错点
- 易错点:指针变量指向常量区数据不可修改
- 二次指针&传递
- 指针常量&常量指针
- 指针与const符结合
- 多维数组的指针
C语言基础—指针
指针定义(&字节长度)
-
使用指针的需求 将某地址保存下来
-
指针使用的场景 传递与偏移
指针定义
在 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字节
❗易错点:指针与指针变量的定义区分
- 指针: 指针是数据对象的
地址
- 指针变量:指针变量是存放某个数据对象地址的
变量
,比如int *p
p本身的地址由编译器另外储存
。存储在哪里,我们并不知道,间接访问数据,首先取得指针变量p的内容,把它作为地址,用来写入或者读取数据。
更多内容见:数组与指针——第三弹
笔试真题- 指针的描述
对指针的描述正确的是( )。
A.指针不能被加减操作
B.在32位操作系统下,指向char类型变量的指针所占内存大小是1个字节
C.指针是存放地址的变量
D.在32位操作系统下,指向int类型变量的指针所占内存大小是4个字节
答案】BD。指针之间可以相减,但不可以相加:两个同一类型的指针变量是可以相减的,他们的意义表示两个指针指向的内存位置之间相隔多少个元素(注意是元素,并不是字节数),故A项错误;指针是数据对象的地址,指针变量是存放某个数据对象地址的变量,故C项错误;根据下表可知BD项表述正确为本题答案。
类型 | 16位平台 | 32位平台 | 64位平台 |
char | 1个字节 | 1个字节 | 1个字节 |
short | 2个字节 | 2个字节 | 2个字节 |
int | 2个字节 | 4个字节 | 4个字节 |
unsigned int | 2个字节 | 4个字节 | 4个字节 |
float | 4个字节 | 4个字节 | 4个字节 |
double | 8个字节 | 8个字节 | 8个字节 |
long | 4个字节 | 4个字节 | 8个字节 |
long long | 8个字节 | 8个字节 | 8个字节 |
unsigned long | 4个字节 | 4个字节 | 8个字节 |
指针 | 2个字节 | 4个字节 | 8个字节 |
指针的传递
- 值传递和地址(引用)传递
值传递
值传递是指在函数调用过程中,将函数外部的变量的值复制给函数内部的形参变量,在函数内部对形参变量的操作并不会影响到函数外部的变量。
举个例子:
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<stdio.h>
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 <stdio.h>
#include <stdlib.h>
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没有意义那样。
- 在工作中,我们把对
指针的加减称为指针的偏移
加就是向后偏移,减就是向前偏移。
举例:
- 数组名a类型是数组,a里边存了一个值,是地址值,是数组的起始地址。
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后不是0x28F769呢?
因为指针变量加1后,偏移的长度是其基类型的长度,也就是偏移sizeof(int)
,这样通过 *(p+1)就可以得到元素a[1].
编译器在编译时,数组取下标的操作正是转换为指针偏移来完成的。
🤔 指针自增自减
既然掌握了指针的使用场景,那么为什么还要了解指针与自增、自减运算符的关系呢?其实,
这就像我们掌握了乘法口诀,但是仍然要做各种乘法运算题一一样。通过一些训练,可以避免在使用中发生错误。
#include <stdio.h>
#include <stdlib.h>
//只有比后增优先级高的操作符,才会作为一个整体, 如()、 []
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呢?
首先,前面讲过当遇到后增操作符时,就要分两步来看。
- 第一步是暂时忽略++操作,即j=* p,因为p指向数组的第一个元素,所以j=2.
- 第二步是对p进行++操作,还是对 * p整体也就是数组第一个元素的值进行 + +操作呢?
这里实际上是对p进行++操作,因为*操作符和++操作符的优先级相同(结合顺序是右往左),只有比++优先级高的操作符才会当成一个整体,目前我们用过的比++操作符优先级高的只有()和[]两个操作符.
❗易错点:指针能相减不能相加
指针相减
指针的运算就是地址的运算。
同类型的二个指针可以相减,其结果为二个指针之间的距离,这个距离是元素个数而不是字节数
例如,如果有一个指向数组第5个元素的指针p和一个指向数组第3个元素的指针q,则可以通过计算p-q得到它们之间相隔两个元素。
举个例子:
#include<stdio.h>
int main(void)
{
double a[2],*p,*q;
p=&a[0]; // p指向a[0]
q=&a[1]; // q指向a[1]
printf("%ld\n", (long int)q - (long int)p); // 内存地址的差,输出8
printf("%ld\n", q - p); // 指针的差,输出1
//因为是double数组(8字节),p指向第一个元素,q指向第二个元素,所以内存地址的差是8字节 输出是8
//指针的差是元素的差,之间隔了一个元素差,所以输出1
}
❗指针相减和内存地址相减的值都是long int类型
在输出指针差时,使用了%ld作为格式说明符
,它表示输出一个long int类型
的整数,因为两个指针相减的结果是一个long int类型
的值,所以需要使用%ld来输出。
- 那么内存地址的差也是一个long int 类型的值吗?
是的,内存地址的差也是一个long int类型的值。指针相减的结果是一个表示两个指针之间相差多少个存储单元的整数值,这个值的类型为long int
。在使用printf函数输出指针相减的结果时,应该使用"%ld"
作为格式说明符来表示一个long int类型的整数。同样地,在将指针类型的值强制转换为long int类型时,也应该使用(long int)进行强制类型转换。
为什么指针相加不能使用
- 指针之间不能进行加法运算,因为两个指针的加法运算的结果是没有意义的。两个指针的加法运算实际上是将它们所指向的内存地址相加,这可能会导致访问无效的内存地址。此外,不同类型的指针具有不同的大小,因此对它们进行加法运算也会导致错误的结果。
指针相加不是没有意义,但是它的用途比较有限,而且需要特别小心。
指针相加的结果是将指针的值加上一个整数,得到一个新的指针值。这个新指针值指向的内存位置不一定有实际意义,也可能导致指针越界或者指向无效的内存位置,从而导致程序出现不可预测的行为或崩溃。因此,一般情况下,不建议使用指针相加操作。
-
指针相加的一种比较有限的用途是,将一个指针指向的位置向后移动若干个元素,以便在数组中遍历或者访问某个元素。这种用法需要保证指针不会越界,并且确保移动的距离不会使指针指向无效的内存位置。
-
如果需要对指针进行加减操作,一般来说应该使用指针的移动运算符(++)和(–),它们可以让指针向前或向后移动一个元素的距离,而且更加安全可靠。
指针比较(同一对象数组)
在C语言中,指针变量的比较有以下几种情况:
-
比较指针是否
相等
:使用等于运算符(==)可以比较两个指针是否指向同一块内存空间。 -
比较指针的
大小关系
:两个指针可以使用大于运算符(>)、小于运算符(<)、大于等于运算符(>=)、小于等于运算符(<=)进行大小关系的比较。当指针指向同一个数组中的元素时,这些运算符可以用来比较指针的偏移量大小。
但是需要注意: 只有二个指针指向同一个数组时候,比较才有意义
| 比较的是元素的地址
- 例子: 通过指针移动和比较遍历数组
//通过指针移动和比较遍历数组
#include<stdio.h>
int main(void){
int a[5]={2,4,6,8,10};
int *p,*q;
p=&a[0];
q=&a[4];
for (; p <=q; p++) // 当指针不大于q,输出p所指的元素的值
{
printf("%d ",*p);
}
printf("%\n");
return 0;
}
指针数组
指针数组定义和应用
指针数组是一种特殊的数组,其中的每个元素都是一个指针。这意味着,指针数组中的每个元素都指向一个变量或内存位置。
在 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"
❗易错点:数组作为函数参数的本质是指针
- 数组名做为实参时,因数组名是
指针常量
,形参
应该是指针变量
,与实参类型相同
也就是说int arr[]== int *arr
下面是一个简单的示例程序,它将一个整型数组作为函数参数,并对其元素进行求和:
//函数实现数组求和,数组名做参数实际上是指针
//下面是一个简单的示例程序,它将一个整型数组作为函数参数,并对其元素进行求和:
#include <stdio.h>
int sum_array(int *arr, int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i];
}
return sum;
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int size = sizeof(arr) / sizeof(arr[0]);
int sum = sum_array(arr, size);
printf("The sum of the array is %d\n", sum);
return 0;
}
在上面的代码中,sum_array()函数的第一个参数是一个整型指针,用于指向整型数组的首地址。函数内部使用arr[i]访问数组元素。在main()函数中,我们将arr和size作为参数传递给sum_array()函数。注意,我们没有使用取地址符&来获取数组的地址,因为数组名已经被转换为指向其首元素的指针。
❗❗ 易错点:注意数组不作函数参数时候,是常量不可修改
//易错点:注意数组不作实参时候,是常量不可修改
#include<stdio.h>
int main(void)
{
int *rear;
int b[5]={1,4,5,7,9};
rear=b+4;
for (; b < rear; b++) //错误,数组名b是常量不可修改值
sum=sum+b;
printf("%d\n",sum);
return 0;
}
指针与一维数组,通过下标和偏移改变元素
指针与一维数组:可以取下标来改变数组元素。
数组名作为实参传递给子函数时,是弱化为指针的
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 运算符创建的对象等。堆空间的特点是随机存取,也就是说,程序可以随时在堆中分配内存,并在需要时释放内存。
总的来说,栈空间更快速,但容量较小,而堆空间容量较大,但访问速度较慢。程序员需要根据实际需要合理使用这两种内存区域。
既然都是内存空间,为什么还要分栈空间和堆空间呢?
栈是计算机系统提供的数据结构,计算机会在底层对栈提供支持:
- 分配专门的寄存器存放栈的地址,压栈操作、出栈操作都有专门的旨令执行,这就决定了栈的效率比较高;
- 堆则是CC艹函数库提供的数据结构,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法请参考关于数据结构、操作系统的书籍)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能由于内存碎片太多),那么就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后返回。
- 显然,堆的效率要比栈低得多栈空间由系统自动管理,而堆空间的申请和释放需要自行管理,所以在具体例子中需要通过free函数释放堆空间。
动态内存申请
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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的申请
首先我们来看malloc函数。
#include <stdlib.h> void *malloc(size_ t size);
需要给malloc传递的参数是一个整型变量,所以这里的size_ t 即为int
; 返回值为void*类型的指针
,void*类型
的指针只能用来存储一个地址而不能进行偏移,因为malloc并不知道我们申请的空间用来存放什么类型的数据,所以确定要用来存储什么类型的数据后,都会将void*强制转换为对应的类型
。
- 在上面例子中我们用来存储字符,所以将其强制转换为char*类型.
所以写的形式是
-
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函数释放堆空间。free 函数的头文件及格式为
#include <stdlib.h> void free(void *ptr);
- 其传入的参数为void类型指针,任何指针均可自动转为void*类型指针,所以我们把p传递给free函数时,
不需要强制类型转换
。
上面例子是:free(p); //free 时必须使用malloc申请时返回的指针值,不能进行任何偏移
其他动态分配函数↓
calloc 函数—分配内存块,且对内存块清0
void *calloc( size_t num, size_t size );
功能: 函数为n个元素的数组分配内存,每个元素长度都是size个字节。如果错误发生返回NULL。
- 举例:此示例从用户处接受一个整数 n,然后使用 calloc 分配一个大小为 n * sizeof(int) 的数组。如果内存分配成功,则打印分配的数组中的所有元素,并释放分配的内存。
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("Enter the number of integers you want to allocate: ");
scanf("%d", &n);
int *arr = (int *) calloc(n, sizeof(int));
if (arr == NULL) {
printf("Failed to allocate memory!\n");
return 1;
}
printf("The array is allocated successfully. The array elements are:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
free(arr);
return 0;
}
realloc重新分配内存空间
void *realloc( void *ptr, size_t size );
功能: 函数将ptr 对象的储存空间改变为给定的大小size。 参数size可以是任意大小,大于或小于原尺寸都可以。 返回值是指向新空间的指针,如果错误发生返回NULL。
- 举例:程序首先创建一个大小为5的动态数组,使用malloc函数分配内存,并且对数组进行初始化。然后,使用realloc函数将数组大小扩展到10,并且初始化新增的元素。最后,程序输出扩展后的数组,并且释放了该数组所占用的内存。
#include <stdio.h>
#include <stdlib.h>
int main() {
int n = 5;
int *p = (int *) malloc(n * sizeof(int));
// 初始化数组
for (int i = 0; i < n; i++) {
p[i] = i + 1;
printf("%d ", p[i]);
}
printf("\n");
// 扩展数组大小
int m = 10;
p = (int *) realloc(p, m * sizeof(int));
// 初始化新增的元素
for (int i = n; i < m; i++) {
p[i] = i + 1;
}
// 输出扩展后的数组
for (int i = 0; i < m; i++) {
printf("%d ", p[i]);
}
printf("\n");
// 释放内存
free(p);
return 0;
}
❗易错点:指针在释放的时候发生偏移
p的地址值必须是malloc当时返回的地址值,不能进行偏移,也就是在malloc和free之间不能进行p++等改变变量p的操作
原因是: 会匹配不上首地址
申请段堆内存空间时,内核帮我们记录的是起始地址和大小,所以释放时内核用对应的首地址进行匹配,匹配不上时,进程就会崩溃。
- 举例说明:
int main() {
int* ptr = (int*) malloc(10 * sizeof(int));
ptr++; // 移动指针
free(ptr); // 错误的释放方式
return 0;
}
在这个例子中,我们分配了一个大小为 10 个整数的内存块,并将指针 ptr 指向该内存块的首地址。然后,我们移动指针 ptr,使其指向内存块中的下一个整数。最后,我们使用 free 函数尝试释放指针 ptr 指向的内存块。
这里的问题是,我们移动了指针 ptr 的位置,使其指向了内存块中的下一个整数,而不是指向该内存块的首地址。这意味着,我们使用 free 函数时,传递给它的指针不再是 malloc 函数返回的首地址,因此会导致运行时错误。
因此,我们应该始终确保使用 free 函数时传递给它的指针与 malloc 函数返回的指针相同,并避免在两者之间进行任何移动或偏移操作。
- 如果要偏移进而存储数据,那么可以定义两个指针变量来解决。
比如,你可以定义一个指针变量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 <stdio.h>
char* print_stack() {
char c[17] = "i am ok";
puts(c);
return c;
}
int main() {
char* p;
p = print_stack();
puts(p);
return 0;
}
执行效果:
- 原因是指针p指向函数,函数是自动申请栈空间。因此栈空间当函数结束会自动释放。 因此指针变量p指向的是释放后的栈空间,乱码。
解决方法:
- 申请动态内存-堆空间。堆空间不会随子函数的结束而释放,必须自己free
#include <stdio.h>
//函数栈空间释放后,函数内的所有局部变量消失
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”"是错误的。
无法把字符数组重新赋其实地址,改变值。
解决方法:索引,strcpy等函数
在这种情况下,您可以使用字符数组的索引来修改字符数组中的每个字符。例如,要将"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 <stdio.h>
#include <stdlib.h>
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 <stdio.h>
#include <stdlib.h>
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指针的值。
函数指针与指针函数
分开区分:
函数指针
函数指针定义
函数指针
:一个指针,指向一个函数
在C语言中,一个函数总是占用一段连续的内存区,函数名
就是该内存区的首地址
,我们可以把这个首地址赋予一个指针变量,通过指针变量来找到并调用这个函数。这个变量称为”函数指针变量
“
函数指针一般表示形式
一般形式为:类型说明符 (*指针变量名)();
-
比如: int (*p)(int *x, int *y);
这里的p不是函数名,而是一个指针变量,它指向一个函数,这个函数有两个指针类型的参数,函数的返回值是int -
int *(*p)(int *x, int *y);
同样的,p是指向一个返回值为int * 类型的函数的指针
使用例程
Q:函数指针如何使用?
int max(int a,int b){
if(a>b)return a;
else return b;
}
main(){
int max(int a,int b);
int(*pmax)(); //定义一个指针
int x,y,z;
pmax=max;//指向函数
printf("input two numbers:\n");
scanf("%d%d",&x,&y);
z=(*pmax)(x,y); //等价于z=max(x,y)
printf("maxmum=%d",z);
}
这里就使用指针pmax指向函数max,也就是函数首地址,然后x,y是参数
z=(*pmax)(x,y) 这样就得到函数返回的int
使用函数指针的好处:
可以将实现同一功能的多个模块统一起来标识,更容易进行后期维护
指针函数
指针函数指的是函数的返回值是一个指针
- 比如:
int *p(int a,int b);
这样函数的返回值是int* 类型的指针
❗❗总结表区分
可以做一个指针使用类型的总结:
这里关于常量指针需要解释下指针常量和常量指针
在讲解const与指针的结合时, 我们先进行区分下指针常量与常量指针
也就是int * const p; 和 const int * p;的区别
❗ 指针常量与常量指针
指针常量 int * const p
- 指针常量——指针类型的常量(int *const p)
本质上一个常量,指针用来说明常量的类型,表示该常量是一个指针类型的常量。在指针常量中,指针自身的值是一个常量,不可改变,始终指向同一个地址
。在定义的同时必须初始化。用法如下:
//一个指针类型的常量 也就是说这个指针是常量,不可修改对象,初始化好的. 但可以修改对象的值内容
int a = 10, b = 20;
int * const p = &a;
*p = 30; // p指向的地址是一定的,但其内容可以修改
常量指针 (const int *p, int const *p)
常量指针本质上是一个指针,常量表示指针指向的内容,说明该指针指向一个“常量”。在常量指针中,指针指向的内容是不可改变的,指针看起来好像指向了一个常量
。用法如下:
//常量指针 int const*p 指针指向的内容是不可改变的 好像指向一个常量 , 指针可以改变对象,不能改变*p,也就是对象的值内容
int a = 10, b = 20;
const int *p = &a;
p = &b; // 指针可以指向其他地址,但是内容不可以改变
更多关于的信息见指针常量和常量指针
例子:
int main()
{
char * const str = "apple";
* str = "orange";
cout << str << endl;
getchar();
}
“apple"是字符串常量放在常量区,str指向"apple”,那么str指向的是字符串常量"apple"的首地址,也就是字符a的地址,因此str指向字符a, * str就等于字符a,对*str的修改就是对字符串首字符a的修改,但"apple"是一个字符串常量,常量的值不可修改。
后面又用 * str= “orange” 来重新给字符串赋值,但是:str由指向字符串常量"apple"的首地址变为指向字符串常量"orange"的首地址,str指向的地址发生了变化,但str是指针常量不能被修改,所以错误。
如果想要程序编译通过,就不能将str声明为指针常量,否则str在初始化之后就无法修改。因此将const修饰符去掉,并修改字符串赋值语句,修改后程序如下:
int main()
{
char * str = "apple";
str = "orange";
cout << str << endl;
getchar();
}
接下来具体讲解下指针与const符号的应用以及指向不同数据的不同可变不可变指针!
❗ 指针指向常量数据(const符)(常量指针和指针常量)
const不仅可以限定变量(包括指针变量)不可以改写,也可限定指针变量所指对象不可改写。
const与指针变量有多种结合方式:
- 指向可变数据的可变指针
- 指向常量数据的可变指针
- 指向可变数据的指针常量
- 指向常量数据的指针常量
指向可变数据的可变指针
先看一个例子:
#include<stdio.h>
#include<Ctype.h>
#include<string.h>
// 这里以字符串字母转换为大写为例子;
void ConvertToUpper(char *sptr)
{
for(;*sptr!='\0';sptr++)
*sptr=toupper(*sptr);
}
int main()
{
char str[3]; //可变数据
for(int i=0;i<3;i++)
{str[i]=getchar();}
ConvertToUpper(str);
printf("%s\n",str);
return 0;
}
str是一个可变数组(可变数据)
, 函数使用的指针也是可变指针
, 具有最高数据访问权限。
**该函数不仅可以改变sptr指针指向的可变数据str的内容,也可以改写sptr的值,指向其他元素. **
指向常量数据的可变指针
比如:
#include<stdio.h>
#include<Ctype.h>
#include<string.h>
//输出字符串, 形参为指向常量const数据的指针变量
void PrintStr(const char *sptr)
{
for(;*sptr!='\0';sptr++)
{
putchar(*sptr);
}
}
int main()
{
const char str[]="abc";
PrintStr(str);
printf("\n");
return 0;
}
这里const修饰的str数组是不可以更改的,sptr指针指向常量数据,不能修改str常量数据的内容,但是可以更改sptr自身的值,指向其他元素.
指向可变数据的指针常量
#include<stdio.h>
#include<Ctype.h>
#include<string.h>
// 这里以字符串字母转换为大写为例子;
void ConvertToUpper(char *sptr)
{
for(;*sptr!='\0';sptr++)
*sptr=toupper(*sptr);
}
//大小写专函,形参为指向可变数据的指针常量
void PrintStr(char * const sptr) //sptr是常量,但指向可变数据
{
//for(;*sptr!='\0';sptr++) //还像上次一样是错误的,const修饰sptr指针,不能指向其他对象,也不能改变作为循环变量
for(i=0;sptr[i]!='\0';i++) /*sptr不可改变,但是这里是使用字符串的一个一个字符来,只是改变里面的内容,不改变对象*/
{
sptr[i]=ConvertToUpper(sptr[i]);
}
}
int main()
{
char str[]="abc";
PrintStr(str);
printf("\n");
return 0;
}
指向常量数据的指针常量
const int *const arr;
这个定义从右向左读作“ptr是一个指针常量、指向一个整型常量”。
用arr做循环变量,或试图改变arr[i]的值,都将导致编译错误
#include<stdio.h>
#include<string.h>
int sum(const int* const arr, const int n)
{
int i,s=0;
for(i=0;i<n;i++)
s=s+arr[i]; //用指针/下标引用数组元素
return(s);
}
int main()
{
int d[3]={1,2,3};
int n=3;
int s;
s=sum(d,n);
printf("%d",s);
printf("\n");
return 0;
}
多维数组指针(以二维数组为例)
例子
求解int(*p)[4]=(int(*)[4])m
;这部分的意思是什么,为什么运行结果是个7?
- p是行指针,
(int(*)[4])是格式类型转换
,目的是为了把一维数组m强行转化成二维数组。 - 因为这个二维数组的第二维大小为4,但是你的m中的元素个数为10,那么你的一维大小为10/4+1=3,即int (*p)[4] 相当于int p[3][4]={{1,2,3,4},{5,6,7,8},{9,0}}
所以p[1][2]=7
下面试试看看例子:
#include <stdio.h>
int main()
{
int m []={1,2,3,4,5,6,7,8,9,0};
int(*p)[4]=(int(*)[4])m;
printf("%d\n",p[1][2]);
return 0;
}
//p是行指针,int(*)[]是格式类型转换,把一维数据m转换为二维数组
// 因为一维数据10个,二维数组的列数已经是4了,所以行数为10/4+1=3行,即int (*p)[4] 相当于int p[3][4]={{1,2,3,4},{5,6,7,8},{9,0}},所以p[1][2]=7
🤔 为什么列数首先知道了,不能省略列数?
这里解释下:
因为二维数组在内存中的地址排列方式是按行排列
的,第一行排列完之后再排列第二行,以此类推。因为,当给出数组的列数时,通过列数与行数的关系,对于这样一个数组,就能找找到特定的地址,从而找到值。
如果只给行而不给列数,那么对于上述的寻址方式,编译器便无法正确的工作,因此在写形参时,需要把二维数组的列数表示出来。
void f(int a[m][n], int m, int n); //合法
void f(int a[][n], int n); //合法
void f(int a[m][], int m); //错误
void f(int a[][], int m); //错误
void f(int a[][], int n); //错误
多维数组指针定义&形式
多维数组指针是指一个指针,它可以指向一个多维数组中的任意一个元素。在C语言中,多维数组可以用一组嵌套的一维数组来实现,因此多维数组指针实际上是指向嵌套的一维数组的指针。
- 形式可以使用*和(来实现:
例子:
关于几维数组指针:
a[i][j][k][2] 等同于 *(*(*(*(a+i)+j)+k)+2) ?
·
维度太高不好理解,以数组int a[i+1][j+1]二维数组为例。
如何取得最后一个元素a[i][j]的值?
a=*a,即该数组的首地址,亦即第0行的首地址
(如果要取第0行的第0个元素,即a[0][0],可用**a
*(a+i)表示第i行首地址,*(a+i)+j第i行第j列的地址。而*表示引用地址的存储内容。
故*(*(a+i)+j)表示i行j列的元素取值。
所以更高维同理。
—— 这里解释下为什么引用是a?**
对于二维数组来说,先通过a来获取第一维的指针,然后再通过这个指针来获取第二维的元素。
我们不能直接改变访问里面某个值,举例说明:
假设我们定义了一个二维数组a:
int a[2][3] = {{1, 2, 3}, {4, 5, 6}};
如果我们想要访问数组a中的某个元素,例如a[1][2],可以直接赋值
int value = a[1][2]; // 直接访问数组元素
也可以使用如下指针:
int **p = a; //指向第一行的指针 = a[0]
int value = *(*(p+1)+2); // 使用指针的指针访问数组元素
p是一个指向指针的指针,它指向了数组a的第一行的指针,也就是指向a[0]的指针。
因此,p+1指向了数组a的第二行的指针,也就是指向a[1]的指针。接着,*(p+1)表示第二行的指针,即a[1],再加上偏移量2,就可以访问a[1][2]这个元素了。
最后直接把它元素值给value值
双重指针的解引用操作 * 在这个表达式中出现了两次。我们可以将它们分开来看:
-
首先是 *(p+1),这里的 p+1 表示将指针 p 的值加上1,也就是让指针指向二维数组的第2行。然后再使用解引用操作符 *,表示获取指向第2行的指针的值,即第2行的首个元素的地址。
-
接着是
*(*(p+1)+2)
,这里的 *(p+1) 表示获取指向第2行的指针的值,即第2行的首个元素的地址。然后再加上2,表示指向该行第3列的元素的地址。最后再使用解引用操作符 *,表示获取该元素的值。
做题思路
整个四维数组的地址
*(a+i) = a[i]
*(a+i)+j = a[i]+j
*((a+i)+j) = a[i][j]
*((a+i)+j)+k = a[i][j]+k
*(*((a+i)+j)+k) = a[i][j][k]
*(*((a+i)+j)+k)+l = a[i][j][k]+l
*(*(*((a+i)+j)+k)+l) = a[i][j][k][l]
不管多少维,按照这个思路,不会差