C语言基础—指针(地址引用、指针数组、二次指针)

news2024/12/30 3:24:45

 

本章主要讲解指针的基本定义和指针的传递、偏移。后面继续讲解指针数组和多维指针、二级指针等

知识点:

  • 指针的定义和指针分类
  • 各类指针的字节长度取决于系统位数
  • 指针的传递(值传递和引用(地址传递))
  • 指针的偏移(自增自减号)
  • 指针数组的应用
  • 指针动态内存申请
  • 字符指针和字符数组的初始化和易错点
  • 易错点:指针变量指向常量区数据不可修改
  • 二次指针&传递

C语言基础—指针

  • 指针定义(&字节长度)
    • 指针定义
      • 字节长度
  • 指针的传递
    • `值传递`
      • 🤔 形参和实参的内存地址一样不?
        • 实参形参是指针时候
      • ❗ 值传递的内存过程
    • `引用传递&`
      • 引用传递的实现
      • 🤔 形参和实参的内存地址一样不?
    • 形参与实参&值传递和地址传递
  • 指针的偏移
    • 定义和应用
    • ❗每次偏移的长度取决数据类型
  • 🤔 指针自增自减
  • 指针数组
    • 指针数组定义和应用
    • 指针与一维数组,通过下标和偏移改变元素
  • 指针与动态内存申请
    • 🤔 内存分配的原因
      • 栈和堆的简单区别
      • 既然都是内存空间,为什么还要分栈空间和堆空间呢?
    • 动态内存申请
      • ❗malloc的申请
      • 🤔 分析进程地址空间
      • free释放内存空间
    • ❗易错点:指针在释放的时候发生偏移
    • 🤔 栈空间与堆空间的差异
      • 指针变量不能指向栈空间
  • 字符指针与字符数组
    • 字符指针与字符数组的初始化
    • ❗❗ 指针变量指向常量区数据,不可以修改
      • 指针可以重新赋地址,字符数组不可以改变对象和地址
        • 解决方法:索引,strcpy等函数
        • 为什么字符数组可以通过索引修改
  • 二次指针
    • 二级指针定义
    • 二次指针的传递


 

指针定义(&字节长度)

  • 使用指针的需求 将某地址保存下来

  • 指针使用的场景 传递与偏移

 

指针定义

在 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<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 的值不会改变。

 

Alt


引用传递&

  • 因为传递过去的是地址,所以然后使用解引用即可。

等于把实参的地址传递过去,形参复制实参地址,改变内容影响实参

 

引用传递的实现

在引用传递中,函数调用时传递的是参数的地址。这意味着,在函数内部对参数的更改会影响到函数调用时传递的参数的值。

可以使用指针实现

#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进行++操作,因为*操作符和++操作符的优先级相同(结合顺序是右往左),只有比++优先级高的操作符才会当成一个整体,目前我们用过的比++操作符优先级高的只有()和[]两个操作符.


指针数组

指针数组定义和应用

 

指针数组是一种特殊的数组,其中的每个元素都是一个指针。这意味着,指针数组中的每个元素都指向一个变量或内存位置。

在 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 运算符创建的对象等。堆空间的特点是随机存取,也就是说,程序可以随时在堆中分配内存,并在需要时释放内存。

总的来说,栈空间更快速,但容量较小,而堆空间容量较大,但访问速度较慢。程序员需要根据实际需要合理使用这两种内存区域。


既然都是内存空间,为什么还要分栈空间和堆空间呢?

栈是计算机系统提供的数据结构,计算机会在底层对栈提供支持:

  • 分配专门的寄存器存放栈的地址,压栈操作、出栈操作都有专门的旨令执行,这就决定了栈的效率比较高;
  • 堆则是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函数往对应的空间存储字符数据。

Alt

  • 首先进行解析 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申请时返回的指针值,不能进行任何偏移


 

❗易错点:指针在释放的时候发生偏移

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  <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指针的值。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/100183.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

动态优化解决方案空间中的最小支持(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 相对于求解函数极值这类静态问题&#xff0c;许多存在于真实世界的优化问题都是在动态变化的&#xff0c;这一类问题被称为动态…

201732-35-6,H2N-AFP-pNA

AFP-pNA&#xff0c;来自牙周病原体牙龈卟啉单胞菌和黑普氏菌的脯氨酸三肽基氨基肽酶的底物。 编号: 189876中文名称: 三肽Xaa-Xaa-Pro tripeptidylpeptidase substrateCAS号: 201732-35-6单字母: H2N-AFP-pNA三字母: H2N-Ala-Phe-Pro-pNA氨基酸个数: 3分子式: C23H27N5O5平均分…

React 入门:脚手架代理配置

文章目录React AjaxAxios在 React 中使用 Axios脚手架代理配置React Ajax 理解 React 本身只关注于界面&#xff0c;并不包含发送 ajax 请求的代码。前端应用需要通过 ajax 请求与后台进行交互&#xff08;json 数据&#xff09;。React 应用中需要继承第三方 ajax 库&#xff…

C++ · 入门 | 准备知识

啊我摔倒了..有没有人扶我起来学习.... &#x1f471;个人主页&#xff1a;《CGod的个人主页》\color{Darkorange}{《CGod的个人主页》}《CGod的个人主页》交个朋友叭~ &#x1f492;个人社区&#xff1a;《编程成神技术交流社区》\color{Darkorange}{《编程成神技术交流社区》…

uniapp实现楼层导航 ,滚动定位,锚点导航

uniapp实现楼层导航的核心技术要点&#xff1a; 1、scroll-view作为视图容器&#xff0c; 2、用其属性scroll-into-view,用于完成点击联动 3、uni.createSelectorQuery().selectAll();获取右侧所有元素信息&#xff0c;获取top值存入数组&#xff0c;用于计算滑动时需要的联动…

Vue-cli工程中每个文件夹和文件的用处

dist 文件夹&#xff1a;默认 npm run build 命令打包生成的静态资源文件&#xff0c;用于生产部署 node_modules&#xff1a;存放npm命令下载的开发环境和生产环境的依赖包 public&#xff1a;有的叫assets&#xff1a;存放项目中需要用到的资源文件&#xff0c;css、js、im…

【Linux】软件包管理器yum

​&#x1f320; 作者&#xff1a;阿亮joy. &#x1f386;专栏&#xff1a;《学会Linux》 &#x1f387; 座右铭&#xff1a;每个优秀的人都有一段沉默的时光&#xff0c;那段时光是付出了很多努力却得不到结果的日子&#xff0c;我们把它叫做扎根 目录&#x1f449;Linux软件…

未来已来,光伏产业将走向何方?十大趋势待揭晓!

碳中和大背景下&#xff0c;光伏已经成为发展最迅猛的热门产业之一。在能源产业变革中&#xff0c;光伏将成为未来最大的绿电来源。 据预测&#xff0c;到2030年&#xff0c;全球可再生能源的占比将超过50%。届时&#xff0c;光伏发电和风电将成为全球可再生能源的主力军。根据…

Android入门第50天-读写本地文件

简介 为了这个系列&#xff0c;我的代码已经准备到了第150天了。接下来的内容会越来越精彩&#xff0c;我们也越来越开始进入Android的一些高级功能上的编程了。今天我们就要讲Android中对本地文件进行读写的全过程。 课程目标 输入文件名、输入文件内容后按【保存到SD卡】&a…

毕业设计 - 基于SSH的任务调度系统的设计与实现 【源码+论文】

文章目录前言一、项目设计1. 模块设计2. 实现效果二、部分源码项目源码前言 今天学长向大家分享一个 Java web 毕业设计项目: 基于SSH的任务调度系统的设计与实现 一、项目设计 1. 模块设计 根据需求调研结果确定本任务调度系统的功能结构&#xff0c;最终系统实现的系统将…

Django

文章目录基础知识创建项目启动项目创建超级用户创建项目构建个人博客网站简单构建开启本地虚拟环境初步创建blog应用常用的模板标签和过滤器注&#xff1a;常用的模板标签注&#xff1a;常用的过滤器模板嵌套全局模板文件夹模板文件设置建议使用css美化页面导航栏页面美化css框…

捋一捋什么是MySQL插入意向锁?

Insert Intention Lock&#xff0c;中文我们也称之为插入意向锁。 这个可以算是对我们之前所讲的 Gap Lock 的一个补充&#xff0c;关于 Gap Lock&#xff0c;如果还有小伙伴不懂&#xff0c;可以参考&#xff1a;聊一聊MySQL的记录锁、间隙锁与 Next-Key Lock。 1. 为什么需…

Java基础之《netty(14)—异步模型》

一、基本介绍 1、异步的概念和同步相对。当一个异步过程调用发出后&#xff0c;调用者不能立刻得到结果。实际处理这个调用的组件在完成后&#xff0c;通过状态、通知和回调来通知调用者。 2、netty中的I/O操作是异步的&#xff0c;包括Bind、Write、Connect等操作会简单的返…

【云原生 | 47】etcdctl客户端的使用方法详解

&#x1f341;博主简介&#xff1a; &#x1f3c5;云计算领域优质创作者 &#x1f3c5;2022年CSDN新星计划python赛道第一名 &#x1f3c5;2022年CSDN原力计划优质作者 &#x1f3c5;阿里云ACE认证高级工程师 &#x1f3c5;阿里云开发者社区专…

后台基础权限框架搭建实现[木字楠博客]

文章目录1、项目整合SpringSecurity1.1、引入SpringSecurity依赖1.2、启动测试1.3、自定义实体类继承UserDetails1.4、自定义配制文件1.5、重写loadUserByUsername方法1.6、自定义匿名访问注解1.8、编写SpringSecurity配制类后台权限框架搭建&#xff1a;本项目权限主要依赖Spr…

Polynomial Round 2022 (Div. 1 + Div. 2, Rated, Prizes!) A-C

比赛链接 目录 A. Add Plus Minus Sign 题意&#xff1a; 思路&#xff1a; 代码&#xff1a; B. Coloring 题意&#xff1a; 思路&#xff1a; 代码&#xff1a; C. Ice and Fire 题意&#xff1a; 思路&#xff1a; 代码&#xff1a; A. Add Plus Minus Sign 题意…

半导体芯片制造过程可以用哪种测量仪器

近年来&#xff0c;面对持续高涨的芯片需求&#xff0c;半导体行业生产迎来了高难度挑战——对芯片工艺要求更精细&#xff0c;从5nm到3nm&#xff0c;甚至是2nm。“先进封装”的提出&#xff0c;是对技术的新要求&#xff0c;也是对封装工艺中材料和设备的全新考验。 芯片身上…

节点电力市场生产商的战略竞标:凸松弛方法(Matlab实现)

目录 1 电力市场 1.1 什么是电力市场 1.2 电力市场发展历程 1.3 对传统电力系统理论的挑战 2 节点电力市场生产商的战略竞标&#xff1a;凸松弛方法 2.1 简介 2 Matlab代码 1 电力市场 1.1 什么是电力市场 市场&#xff1a;商品交换(交换商品的过程称交易)关系的总和 …

pikachu靶场验证码绕过详解

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是pikachu靶场验证码绕过详解。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#xff1a;严禁对…

endo-BCN-PEG3-Biotin,endo-BCN三聚乙二醇-生物素

【中文名称】endo-BCN三聚乙二醇-生物素 【英文名称】 endo-BCN-PEG3-Biotin&#xff0c;BCN-PEG3-Biotin (endo) 【CAS号】1263166-92-2 【分子式】C29H46N4O7S 【分子量】594.77 【基团部分】BCN 【纯度标准】95% 【外观颜色】 白色固体 &#xff08;具体由其分子量大小决定…