6 C 语言指针的奥秘:理论与实践详解

目录

1 变量访问机制

1.1 内存地址

1.2 变量的直接访问

1.3 变量的间接访问

2 指针变量及其内存大小

2.1 指针与指针变量

2.2 指针变量的定义格式

2.3 指针变量的内存大小

3 取地址操作符与取值操作符

3.1 取地址操作符 &

3.2 取值操作符 *

3.3 解引用与数据类型大小

4 注意事项

4.1 指针变量名

4.2 指针变量的类型指定

4.3 &* 与 *&运算

4.4 指针声明的清晰性

5 指针的传递

5.1 传递变量

5.2 进程地址空间原理图

5.3 传递指针

6 指针的偏移

6.1 指针的加法与减法

6.2 指针与一维字符数组

7 动态内存申请与释放

7.1 malloc 和 free

7.2 杜绝以 free 函数释放偏移指针

7.3 动态设置数组长度

7.4 指针大小与指向内存空间的大小

8 栈空间和堆空间

8.1 栈空间(Stack)

8.2 堆空间(Heap)

8.3 栈与堆的数据结构特点

8.4 栈/堆时间有效性的差异

9 相关重要概念

9.1 野指针

9.2 悬垂指针

9.3 空指针解引用

9.4 指针越界

10 本章判断题

11 OJ 练习

11.1 课时6作业1

11.2 课时6作业2


1 变量访问机制

1.1 内存地址

在计算机的内存中,每一个字节都被赋予了一个唯一的编号,这个编号就被称为“地址。内存地址是计算机内部用于标识和访问数据位置的一种机制。当我们编写程序并定义变量时,这些变量在编译过程中会被分配内存单元(内存中的特定位置),每个位置都对应着一个唯一的内存地址。

1.2 变量的直接访问

直接访问是程序中最常见的变量访问方式。在  C 语言中,当我们直接使用变量名进行读写操作时,如 scanf("%d",&i);  或 printf("%d", i);  实际上是通过变量名背后的内存地址来直接存取变量的值。编译器在编译过程中会解析变量名,并将其转换为对应的内存地址,从而实现对变量值的直接操作。

直接访问是通过变量名直接存取变量值。

1.3 变量的间接访问

与直接访问相对应的是间接访问。在间接访问中,我们不直接使用变量的名字来存取其值,而是使用一个特殊的变量——指针变量存储目标变量的地址。指针变量本身也是一个变量,但它存储的是其他变量的内存地址而非直接的数据值。

间接访问是通过指针变量来间接存取变量的值。

通过这种方式,我们可以先获取到某个变量的地址,然后通过这个地址来访问或修改该变量的值。例如,在 C 语言中,我们可以使用 & 运算符来获取变量的地址,并将其存储在指针变量中,如 int *pointer = &i;  之后,我们就可以通过 *pointer 来访问或修改 i 的值,实现了对 i 的间接访问。


指针变量及其内存大小

2.1 指针与指针变量

指针一个变量的地址称为该变量的“指针”。指针是一个地址的概念,它指向内存中的一个位置。在 C 语言中,我们不能直接操作这个地址,但可以通过指针变量来存储和操作它。

指针变量是一个特殊的变量,专门用来存放另一个变量的地址(即指针)它的值是一个地址。这个地址可以是另一个变量的地址、数组元素的地址、结构体成员的地址等。通过指针变量,我们可以间接访问和操作它所指向的内存位置中的数据。

指针变量是 C 语言中的一个核心概念,它赋予了程序直接操作内存地址的能力。通过指针,我们可以实现数组的动态遍历、内存的动态分配与释放、函数参数的引用传递等高级功能。然而,指针的使用也伴随着一定的风险,如野指针、空指针解引用、指针越界等问题,这些都需要程序员在编程过程中格外注意。

2.2 指针变量的定义格式

指针变量的定义格式遵循以下基本规则:

类型名 *指针变量名;
  • 类型名:表示指针指向的变量的数据类型。例如,如果指针将指向一个整型变量,则类型名为int。
  • *:星号(或称为指针操作符用于声明一个变量为指针变量
  • 指针变量名:这是指针变量的名称,可以通过这个名称来访问指针变量本身或者它所指向的变量的值(使用解引用操作符 * )。

示例:

int num = 10;  // 定义一个整型变量 num  

// 类型名 *指针变量名;
int *pointer ;      // 定义一个整型指针 pointer (指针变量名)  
pointer = #    // 将 num 的地址赋给 pointer 

printf("%d\n", *pointer ); // 输出 pointer 所指向的变量的值,即 num 的值,输出为 10

2.3 指针变量的内存大小

在 C 语言中,指针变量本身所占用的内存空间大小取决于程序运行的平台(主要是操作系统的位数)。指针的主要作用是存储内存地址,因此其大小通常与该系统内存地址的最大可能值所需的位数相匹配。

64位程序:在64位操作系统上编译的64位程序,其指针通常占用 8 字节(64位,1 字节= 8位)的内存空间。这是因为64位系统允许的最大内存地址空间远大于4GB,需要64位(即8字节)来唯一标识。

32位程序:在32位操作系统上编译的32位程序,其指针占用 4 字节(32位,1 字节= 8位)的内存空间。这是因为32位系统允许的最大内存地址空间为4GB,足以用32位(即4字节)来表示。

考研中往往会显示的强调程序是32位的程序,即 sizeof(i_pointer) = 4

假设我们有一个指针变量 i_pointer,其类型取决于它指向的数据类型,但无论指向什么类型,  i_pointer 本身作为指针在内存中的占用大小只与程序的位数有关,如下示例:

#include <stdio.h>

int main() {
    int *i_pointer; // i_pointer是一个指向int的指针
    // 使用sizeof运算符获取i_pointer的内存占用大小
    printf("%zu\n", sizeof(i_pointer)); 

    return 0;
}
  • 如果上述代码是在一个 64 位系统上编译的 64 位程序,那么 sizeof(i_pointer) 的输出将是 8。
  • 如果上述代码是在一个 32 位系统上编译的 32 位程序,那么 sizeof(i_pointer) 的输出将是 4。 

3 取地址操作符与取值操作符

3.1 取地址操作符 &

定义:取地址操作符 & 也称为引用,用于获取变量的内存地址。

作用当对变量使用 & 操作符时,它会返回该变量在内存中的地址。这个地址是一个无类型的值(但在使用时,我们通常将其视为指向某种类型的指针)。

3.2 取值操作符 *

定义:取值操作符 * 也称为解引用操作符,用于访问指针所指向地址中的数据。

作用当对指针使用 * 操作符时,它会返回指针所指向地址中的数据。这个过程称为解引用

取地址和引用这两个操作互为逆过程。

示例: 

#include <stdio.h>

int main() {
    int i = 5;
    int *i_pointer = &i; // 指针变量的初始化一定是对某个变量取地址,不能随机写个数
   
    // int *i_pointer;
    // i_pointer = &i;  这两行代码 等价于上面的 int *i_pointer = &i;

    printf("i=%d\n", i);  // 直接访问变量i的值 5
    printf("*i_pointer=%d\n", *i_pointer); // 间接访问变量i的值,通过指针i_pointer 5

    return 0;
}

在上面的代码中,&i 获取了整型变量 i 的内存地址,并将这个地址赋值给了整型指针 i_pointer。

*i_pointer 访问了指针 i_pointer 所指向的内存地址中的数据,即变量 i 的值。由于 i_pointer 是一个指向整型(int)的指针,因此 *i_pointer 返回的是一个整型值。 

3.3 解引用与数据类型大小

当通过指针取值(即解引用指针)时,所获取的“空间大小”是由指针所指向的数据类型决定的这一机制确保了通过指针访问的数据类型与指针本身声明的类型一致。

在上面的示例代码中,指针 p 存储了变量 i 的内存地址。当使用 *p 时,实际上是在告诉编译器:“去 p 指向的那个地址,把那里的数据作为整型来读取。” 由于 p 是一个指向整型(int)的指针,编译器知道它需要读取 4 字节的数据,并将其解释为一个整型值

因此,当通过 *p 访问数据时,是请求编译器去访问指针所指向的内存地址,并读取那里的 4 字节数据(因为那是一个 int 整型值)。


4 注意事项

4.1 指针变量名

指针变量前面的 “*” 表示该变量为指针型变量,我们经常说的指针变量名不包括 *

例如,下面代码中指针变量名是 pointer_1 ,而不是 *pointer_1 :

类型名 *指针变量名;

float *pointer_1;

4.2 指针变量的类型指定

在定义指针变量时必须明确指定其类型。这意味着指针只能存储与其类型相匹配的变量的地址

例如,指向整型数据的指针不能用来存储浮点型变量的地址。错误示例如下:

float a;  
int *pointer_1;  
pointer_1 = &a; // 这是错误的,因为pointer_1是int*类型,而&a是float*类型

4.3 &* 与 *&运算

“&” 和 “*”两个运算符的优先级别相同,但要按自右向左的方向结合。

#include <stdio.h>

int main() {
    int a = 5;
    int *pointer;
    pointer = &a;

    printf("&a=%p\n", &a); // &a=000000000061FE14
    printf("pointer=%p\n", pointer); // i=000000000061FE14

    printf("a=%d\n", a); // a=5
    printf("*pointer=a=%d\n", *pointer); // *pointer=a=5

    printf("&*pointer=&a=%p\n", &*pointer); // &*pointer=&a=000000000061FE14
    printf("*&a=*pointer=%d\n", *&a); // *&a=*pointer=5

    return 0;
}

&*运算:当执行 pointer = &a; 之后,&*pointer 的含义从逻辑上看似是先通过 *pointer 对 pointer 所指向的地址进行解引用,获取该地址中的值(即 a 的值),但紧接着的 & 操作试图获取这个值的地址。然而,在大多数情况下,这个操作是无意义的,因为解引用得到的是一个值(如 int),而不是一个可以取地址的左值(lvalue。如果编译器允许这样的操作(尽管它可能会警告或优化掉),那么结果可能是一个指向临时值的指针,这是不安全的。但在数值上(如果忽略类型和安全性问题),&*pointer 和 &a 会相同。在实际编程中,应避免编写像 &*pointer 这样的代码。

*& 运算:*&a 首先通过 &a 获取 a 的地址,然后通过 * 对该地址进行解引用,从而得到 a 的值。这是一个有意义的操作,因为 &a 产生了一个可以解引用的指针。因此,*&a 等价于 a。

4.4 指针声明的清晰性

C 语言本质上是一种自由形式的语言,这很容易诱使我们把 “*” 写在靠近类型的一侧,同时 C 语言也允许将 * 写在靠近类型的一侧,例如:

int* a  等价于  int *a

但这可能导致误解,尤其是当声明多个变量时。例如:

int* a, b, c; // 容易误会成把所有三个变量声明为指向整型的指针

上面的语句中,实际上只将 a 声明为指向整型的指针,而 b 和 c 是普通的整型变量。

为了避免混淆,应该明确地为每个指针变量添加 * 符号,例如:

int *a, *b, *c; // 建议明确地为每个指针变量添加 * 符号

5 指针的传递

5.1 传递变量

很多初学者不喜欢使用指针,觉得使用指针容易出错,其实这只是因为没有掌握指针的使用场景。经过多年的实战经验总结,指针的使用场景通常只有两个,即传递偏移,读者应时刻记住只有在这两种场景下使用指针,才能准确地使用指针。多加练习之后,我们会发现指针其实很简单。下面我们来看指针的传递使用场景。

#include <stdio.h>

// 在子函数中尝试改变主函数中某个变量的值
void change(int j) { // 实参初始化形参 相当于 j=i
    j = 5;
}

int main() {
    int i = 10;
    printf("before change i = %d\n", i); // 这里打断点
    change(i); // 在这一步按 下箭头,进入 change 函数
    printf("after change i = %d\n", i); // 这里打断点

    return 0;
}

在上面例子的主函数中,定义了整型变量 i ,其值初始化为 10,然后通过子函数修改整型变量 i 的值。但是,我们发现执行语句 printf("after change i = %d\n", i);  后,打印的 i 的值仍为10,子函数 change 并未改变变量 i 的值。下面我们通过调试程序来查看为什么会出现这种情况:

在上面例子的代码提示位置打断点,然后调试程序,在内存视图中输入 &i ,可以看到变量 i 的地
址是0x61fe1c。按步入调试按钮(向下键头)或 F7 进入 change 函数,这时变量 j 的值的确为10(实参 i 初始化了形参 j),但是 &j 的值为 0x61fdf0,也就是 j 和 i 的地址并不相同。运行 j=5 后,change 函数实际修改的是地址 0x61fdf0 上的值,从10变成了5,接着 change 函数执行结束,变量 i 的值肯定不会发生改变,因为变量 i 的地址是 0x61fe1c 而非 0x61fdf0 (由于程序每次重新编译,因此大家那里的地址和我这里可能是不一样的,这个没关系,关键是观察 i 和 j 的地址不一样)。

这是因为 C 语言使用值传递(pass by value)的方式传递函数参数。这意味着当你将 i 作为参数传递给 change 函数时,实际上是将 i 的值(在这个例子中是 10)赋值(j=i)给了 j。在 change 函数内部,对 j 所做的任何修改都不会影响到原始的 i 变量。

5.2 进程地址空间原理图

程序的执行过程其实就是内存的变化过程,我们需要关注的是栈空间的变化。

在 5.1小节的代码中,当 main 函数开始执行时,系统(或更准确地说,是操作系统的运行时环境)会为 main 函数开辟函数栈空间,这块栈空间用于存储 main 函数执行过程中所需的所有局部变量、参数、返回地址等信息。

当程序执行到 int i = 10; 时,main 函数的栈空间中会为变量 i 分配 4 字节(这取决于编译器和目标平台的整型大小,但大多数情况下是 4 字节)的空间,并将值 10 存储在这个位置。

当 main 函数调用 change 函数时,系统会为 change 函数分配一块新的、独立的栈空间。这块栈空间用于存储 change 函数执行过程中所需的所有局部变量、参数等信息,并为形参变量 j 分配 4 字节大小的空间。

在调用 change(i); 时,实际上是将 main 函数栈空间中的 i 的值(在上面例子中是 10)赋值给了 change 函数栈空间中的形参 j。这就是所谓的值传递(C语言的函数调用均为值传递)。j 只是在 change 函数的栈空间中存在的一个局部变量,它与 main 函数栈空间中的 i 是完全独立的。

在 change 函数内部,对 j 的任何修改都不会影响到 main 函数栈空间中的 i。当 change 函数执行完毕并返回时,其栈空间会被释放,包括其中的局部变量 j。此时,main 函数继续执行,i 的值仍然是最初赋给它的 10。

原理图(进程地址空间)如下所示:

程序启动起来后被称为进程

进程的定义

  • 狭义定义:进程是正在运行的程序的实例。
  • 广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

5.3 传递指针

有的读者会问,难道就不能在子函数中修改 main 函数内的某个变量的值?答案是可以的,我们将程序进行了如下所示的修改:将 i 的地址传递给 change 函数,并在函数内部通过解引用指针来修改值。

#include <stdio.h>  

void change(int *j) {  // 实参初始化形参 相当于 j=&i
    *j = 5; // 通过解引用指针来修改值  
}  

int main() {  
    int i = 10;  
    printf("before change i = %d\n", i);  
    change(&i); // 传递 i 的地址  
    printf("after change i = %d\n", i);  

    return 0;  
}

我们可以看到程序执行后,语句 printf("after change i=%d\n",i); 打印的 i 的值为5,难道 C 语言函数调用值传递的原理变了?

并非如此,我们将变量 i 的地址传递给 change 函数时,实际效果是 j=&i,依然是值传递,只是这时我们的 j 是一个指针变量,内部存储的是变量 i 的地址,所以通过 *j 就间接访问到了与变量 i 相同的区域,通过 *j=5 就实现了对变量 i 的值的改变


6 指针的偏移

前面介绍了指针的传递。指针即地址,就像我们找到了一栋楼,这栋楼的楼号是B,那么往前就是A,往后就是C,所以应用指针的另一个场景就是对其进行加减,对指针进行乘除是没有意义的,就像家庭地址乘以 5 没有意义那样。

在 C 语言中,指针的偏移(把对指针的加减称为指针的偏移,加就是向后偏移,减就是向前偏移)是一个非常重要的概念,它允许我们通过对指针进行算术运算(主要是加法和减法)来访问数组或其他连续分配的内存块中的元素。这种操作直接基于指针所指向的数据类型的大小

6.1 指针的加法与减法

当我们对指针进行加法或减法操作时,实际上是对指针所指向的内存地址进行偏移。偏移的量不是以字节为单位,而是以指针所指向的数据类型的大小为单位。

例如,如果有一个指向 int 类型的指针(假设 int 占 4 字节),那么对该指针加 1,实际上是将指针的地址向前移动了 4 个字节,因为 int 类型占 4 个字节。

#include <stdio.h>  
// 指针的偏移使用场景,也就是对指针进行加和减操作  
  
#define N 5 // 定义数组的大小  
  
int main() {  
    // 定义一个整型数组a,数组名a在表达式中会被转换为指向数组首元素的指针 ,即a中存储的是数组第一个元素的地址  
    int a[N] = {1, 2, 3, 4, 5};  
    int *p; // 定义一个整型指针变量p  
    p = a; // 将数组a的首地址赋给指针p,现在p指向了数组的第一个元素  
    
    int i;  
    // 通过指针p遍历数组,并打印每个元素的值  
    // *(p + i) 相当于 a[i],表示取指针p指向的地址向后偏移i个整型大小的位置上的值  
    for (i = 0; i < N; i++) {  
        printf("%3d", *(p + i)); // 这里写a[i]是等价的,都是访问数组的第i个元素  
    }  
  
    printf("\n-----------------\n");  
  
    // 将指针p指向数组的最后一个元素  
    p = &a[4];  
  
    // 注意:这里的循环虽然可以打印出数组的元素,但存在潜在的风险  
    // 当i大于0时,p-i会指向数组前面的元素,这是合法的。  
    // 但是,如果数组索引是负数(即p向前偏移超过了数组的首地址),则行为是未定义的。  
    // 在这个例子中,由于我们是从数组的末尾开始,所以不会超出数组界限。  
    for (i = 0; i < N; i++) {  
        printf("%3d", *(p - i)); // 这里写a[N-1-i]是逻辑上等价的,都是从数组的末尾向前遍历  
    }  
    printf("\n");  
    return 0;  
}

如下图所示,数组名中存储着数组的起始地址 0x61fdf0,其类型为整型指针,所以可以将其赋值给整型指针变量 p,可以从监视窗口中看到 p+1 的值为 0x61fdf4.那么为什么加1后不是 0x61fdf1  呢?因为指针变量加1后,偏移的长度是其基类型的长度,也就是偏移sizeof(int),这样通过 *(p+1) 就可以得到元素a[1]

6.2 指针与一维字符数组

为什么一维数组在函数调用进行传递时,它的长度子函数无法知道呢?这是由于在C语言中,一维数组名在大多数情况下会被当作指向其首元素的指针当数组名作为实参传递给函数时,它实际上传递的是数组首元素的地址(即指向该元素的指针。因此,对数组名进行算术运算(如加或减)实际上是对数组首元素的地址进行偏移。

数组的下标访问(如a[i])在底层实现时,就是通过指针偏移来完成的。具体来说,a[i] 等价于 *(a + i),即先找到数组首元素的地址,然后加上 i 个元素大小(即 i * sizeof(数组元素类型))的偏移量,最后解引用这个地址得到元素的值。

如下例所示,数组名 c 中存储是一个起始地址,所以子函数 change 中其实传入了一个地址(即 &c[0] )。定义一个指针变量时,指针变量的类型要和数组的数据类型保持一致,通过取值操作,就可将“h”改为“H”,这种方法称为指针法。获取数组元素时,也可以通过取下标的方式来获取数组元素并进行修改,这种方法称为下标法

#include <stdio.h>

// 数组名传递给子函数时,是弱化为指针的
// 函数用于修改传入的字符串的前三个字符为'H', 'E', 'E'  
void change(char *d) {// 这里不是 char d[],而是使用指针的形式
    // 使用指针的解引用修改第一个字符  
    *d = 'H';
    // 使用下标法修改第二个字符  
    d[1] = 'E';
    // 再次使用指针的算术运算和解引用修改第三个字符  
    *(d + 2) = 'E';
}

int main() {
    char c[10] = "hello"; // 初始化一个字符数组  
    change(c); // 传递数组名(实际上是数组首元素的地址)给函数  
    puts(c); // 输出修改后的字符串  
    
    return 0;
}

*d 是对指针 d 的解引用,即获取 d 指向位置的值,并可以对其进行修改。

d[1] 和 *(d + 1)是等价的,都是访问指针 d 向后偏移一个单位(这里是 char 类型,即一个字节)所指向的值。


7 动态内存申请与释放

7.1 malloc 和 free

在 C 语言中,malloc 和 free 是两个非常重要的函数,它们分别用于动态内存分配和释放。这两个函数是 C 标准库(stdlib.h)的一部分,为程序员提供了在程序运行时根据需要分配和释放内存的能力。

malloc 函数

malloc 函数的全称是 memory allocation,用于动态地分配指定大小(以字节为单位)的内存块。其原型定义在 stdlib.h 头文件中。

#include <stdlib.h>

void* malloc(size_t size);
  • 参数:size 指定了要分配的内存块的大小(以字节为单位)
  • 返回值如果分配成功,malloc 返回一个指向分配的内存块的指针(一个堆空间的首地址)如果分配失败(通常是因为可用内存不足),则返回 NULL
  • 由于 malloc 不知道这块内存将用于存储什么类型的数据,所以它只能返回一个 void* 类型的指针。 void* 类型的指针不能偏移(类型都不知道,偏移多少就不得而知了)当对 void* 指针所指向的内存进行操作时,需要首先将它强制转换为具体类型的指针。

使用 malloc 时,需要注意以下几点:

  • 分配的内存块的内容是未初始化的,即它们的值是未定义的。如果需要,应该在使用之前手动初始化这些内存。
  • 分配的内存块的大小是通过参数 size 指定的,这个大小是固定的,一旦分配就不能改变。如果需要更大的内存块,需要先释放当前的内存块,然后重新分配一个更大的内存块。
  • 分配的内存块在程序的整个生命周期内都是有效的,直到显式地调用 free 函数释放它。
  • 最好对 malloc 的返回值进行检查,因为 malloc 可能在无法分配请求的内存时返回 NULL。如果不检查返回值就直接使用,程序可能会在尝试解引用 NULL 指针时崩溃。

free 函数

free 函数用于释放之前通过 malloc、calloc 或 realloc 分配的内存块(必须是最初的地址,补不可偏移)。其原型也定义在 stdlib.h 头文件中。

#include <stdlib.h>

void free(void* ptr);
  • 参数ptr 是指向要释放的内存块的指针。这个指针必须是之前通过 malloc、calloc 或 realloc 成功分配的内存块的指针(不能是偏移之后的地址!!!)
  • 返回值:free 函数没有返回值。
  • 传入 free 函数的参数为 void * 类型指针,任何指针均可自动转为 void* 类型指针,所以我们把指针变量传递给 free 函数时,不需要强制类型转换

使用 free 时,需要注意以下几点:

  • 一旦调用了 free,指针 ptr 指向的内存块就被释放了,但是 ptr 指针本身的值(即内存地址)并没有改变。因此,在调用 free 之后,不应该再访问 ptr 指向的内存块,因为它现在可能已经被操作系统重新分配给其他程序使用。为了避免潜在的错误,一个好的习惯是将 ptr 设置为 NULL,表示它不再指向任何有效的内存块
  • 可以多次释放同一个内存块(尽管这通常是一个编程错误),但结果未定义。为了避免这种情况,你应该确保每个内存块只被释放一次
  • free 不会释放指针本身所占用的内存,它只释放指针所指向的内存块。
  • free 函数不可以释放偏移后的地址,如果对指针进行了偏移操作,然后将偏移后的指针传递给 free 函数,free 函数将无法找到正确的管理信息,可能导致无法正确释放内存,甚至可能引发程序崩溃或出现内存泄漏等问题。

示例:

#include <stdlib.h>
#include <stdio.h>

int main() {
    // 当对 void* 指针所指向的内存进行操作时,需要首先将它转换为具体类型的指针。
    int *ptr = (int*)malloc(sizeof(int)); // 动态分配一个int大小的内存块

    // 对 malloc 的返回值进行检查是一个非常重要的步骤
    if (ptr != NULL) {
        *ptr = 10; // 使用这块内存
        printf("Value: %d\n", *ptr); // Value: 10
        free(ptr); // 释放这块内存

        ptr = NULL; // 避免野指针
    }

    return 0;
}

对 malloc 的返回值进行检查是一个非常重要的步骤,因为 malloc 可能在无法分配请求的内存时返回 NULL。如果不检查返回值就直接使用,程序可能会在尝试解引用 NULL 指针时崩溃。 

为了避免潜在的错误,一个好的习惯是在 free 后将 ptr 设置为 NULL,表示它不再指向任何有效的内存块。

7.2 杜绝以 free 函数释放偏移指针

当对通过 malloc、calloc 或 realloc 获得的指针进行偏移操作(例如 p = p + offset;),并将偏移后的指针传递给 free 函数时,free 函数将无法找到与该偏移指针相对应的内存管理信息(如内存块的起始地址和大小)。

内存管理库(如 C 标准库中的内存管理函数)会跟踪每个通过 malloc 等函数分配的内存块的起始地址和大小。当调用 free 时,它期望提供的是原始分配时返回的指针(即内存块的起始地址)。如果提供了一个偏移后的指针,free 可能无法找到或错误地解释这些信息,导致未定义行为

未定义行为可能包括:

  • 程序崩溃:如果 free 试图访问或修改无效的内存地址,操作系统可能会终止程序以防止进一步的损害。
  • 内存泄漏:在某些情况下,free 可能不会释放任何内存,或者会释放错误的内存块,导致原始分配的内存块未被释放,从而造成内存泄漏。
  • 数据损坏:free 可能会错误地修改内存管理结构或相邻的内存块,导致程序中的其他部分出现不可预测的行为。
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = (int *) malloc(sizeof(int) * 10);

    // 错误操作:偏移指针 p
    p = p + 1;

    // 执行释放内存操作,此时会导致进程崩溃
    free(p);
    p = NULL;

    return 0;
}

运行结果异常:

为了避免这些问题,请确保始终使用 malloc 等函数返回的原始指针来调用 free。如果实在需要在内存块中进行偏移以访问或修改数据,可以创建一个新的指针变量来保存偏移后的地址,但保留原始指针用于释放内存。如下所示:

#include <stdlib.h>  
  
int main() {  
    int *originalPtr = malloc(10 * sizeof(int)); // 分配内存,并保存原始指针  
    if (originalPtr == NULL) {  
        // 处理内存分配失败的情况  
        return 1;  
    }  
  
    // 创建一个偏移指针,指向中间位置  
    int *offsetPtr = originalPtr + 5;  
  
    // ... 使用 offsetPtr 进行数据访问或修改 ...  
  
    // 使用原始指针来释放内存  
    free(originalPtr); // 正确释放内存  
  
    // 注意:不要对 offsetPtr 进行 free 操作  
  
    return 0;  
}

运行结果正常:

在这个示例中,originalPtr 被用于 free 调用,offsetPtr 仅仅是一个指向 originalPtr 所分配内存块中某个位置的指针,它本身并不拥有或管理任何内存。因此,不需要(也不应该)对 offsetPtr 进行 free 操作。

简而言之,只有那些通过 malloc 家族函数获得的原始指针才应该被 free 释放。其他任何从这些原始指针派生出来的指针(如通过加法、减法或数组索引得到的偏移指针)都不应该被 free。

7.3 动态设置数组长度

很多读者在学习 C 语言的数组后都会觉得数组长度固定很不方便,其实 C 语言的数组长度固定是因为其定义的整型、浮点型、字符型变量、数组变量都在栈空间中,而栈空间的大小在编译时是确定的。如果使用的空间大小不确定,那么就要使用堆空间。

#include <stdio.h>
#include <stdlib.h> // 引入stdlib.h头文件,因为它包含了malloc和free函数的定义  
#include <string.h> // 引入string.h头文件,因为它包含了strcpy和puts函数的定义  

int main() {
    int size; // 定义整型变量size,用于存储用户希望申请的空间大小(以字节为单位)  
    char *p; // 定义字符指针p,用于指向动态分配的内存区域  

    // 读取用户输入的空间大小  
    scanf("%d", &size);

    // 使用malloc函数动态分配内存。malloc返回的是void*类型,表示指向任意类型的指针。  
    // 但由于我们打算存储字符数据,所以需要将void*转换为char*。  
    // 注意这里的长度,不要忘了字符数组最后有一个结束符
    p = (char *) malloc(size);

    if (p != NULL) {

        // 使用strcpy函数将字符串"malloc success"复制到p指向的内存区域。  
        // 注意:这里假设用户输入的size足够大以存储这个字符串(包括结尾的空字符'\0')。  
        // 如果size小于字符串长度(包括结尾的空字符),这会导致缓冲区溢出,是未定义行为。  
        strcpy(p, "malloc success");

        // 使用puts函数输出p指向的字符串  
        puts(p);

        // 使用free函数释放p指向的内存区域。  
        // 重要的是,传递给free的地址必须是之前通过malloc、calloc或realloc获得的地址。  
        free(p);

        // 将p设置为NULL,这是一个好习惯,可以避免成为野指针(即指向已经被释放的内存的指针)。  
        // 尝试解引用野指针会导致未定义行为,通常使程序崩溃。  
        p = NULL;

        // 输出提示信息,表明内存释放成功  
        printf("free success\n");
    }
    return 0;
}

注意:

1、使用 malloc 函数动态分配内存。malloc 返回的是 void* 类型,表示指向任意类型的指针。但由于我们打算存储字符数据,所以需要将 void* 转换为 char*。

2、传入 free 函数的参数为 void * 类型指针,任何指针均可自动转为 void* 类型指针,所以我们把指针变量传递给 free 函数时,不需要强制类型转换。 

3、释放申请的内存后,最好再将 p 设置为 NULL,这是一个好习惯,可以避免成为野指针(即指向已经被释放的内存的指针)。

如下图所示,在上面的代码中,定义的整型变量 i (上述代码中是 size )、指针变量 p 均在 main 函数的栈空间中,通过 malloc 申请的空间会返回一个堆空间的首地址,我们把首地址存入变量 p。知道了首地址,就可以通过 strcpy 函数往对应的空间存储字符数据

7.4 指针大小与指向内存空间的大小

指针本身的大小(通常是 4 字节(32位系统)或 8 字节(64位系统))与其指向的内存空间大小是两个不同的概念。指针仅存储内存地址,而该地址指向的内存空间大小由程序员通过 malloc 等函数的参数指定。

#include <stdio.h>
#include <stdlib.h>

int main() {
    int* ptr;  // 定义一个整数指针

    printf("指针 ptr 的大小: %zu 字节\n", sizeof(ptr));  // 输出指针本身的大小

    ptr = (int*)malloc(sizeof(int) * 5);  // 分配 5 个整数大小的内存空间

    printf("分配的内存空间大小: %zu 字节\n", sizeof(int) * 5);  // 输出分配的内存空间大小

    free(ptr);  // 释放分配的内存
    ptr = NULL; // 避免成为野指针

    return 0;
}

在上述代码中,首先通过 sizeof(ptr) 输出了指针 ptr 本身的大小,通常在 32 位系统中为 4 字节,在 64 位系统中为 8 字节。然后使用 malloc 函数分配了一定大小的内存空间,并通过计算 sizeof(int) * 5 输出了分配的内存空间大小。

所以不要在调试窗口试图通过:sizeof(p) 得到动态创建内存的大小,这将永远返回指针本身的大小(4字节 或 8字节)


8 栈空间和堆空间

8.1 栈空间(Stack)

定义与用途:

栈是一种遵循后进先出(LIFO, Last In First Out)原则的数据结构。在C语言中,栈空间主要用于存储局部变量、函数参数、返回地址等。每当函数被调用时,其局部变量和参数等信息会被压入栈中;当函数执行完毕返回时,这些信息会从栈中弹出,栈顶指针下移。

特点:

  • 自动管理栈空间的分配和释放由编译器自动完成,程序员无需手动干预
  • 大小有限:栈空间的大小通常是固定的,如果递归调用过深或局部变量过多,可能会导致栈溢出(Stack Overflow)。
  • 速度快:由于栈空间的分配和释放是自动的,且通常位于连续的内存区域,因此访问速度非常快

示例:

void func() {  
    int a = 10; // 局部变量a存储在栈上  
    // 函数执行完毕后,a的存储空间会自动释放  
}  
  
int main() {  
    func(); // 调用func时,其局部变量a会被压入栈中  

    return 0;  
}

8.2 堆空间(Heap)

定义与用途

堆空间是程序运行时用于动态分配内存的区域。与栈空间不同,堆空间的大小不是固定的,程序员可以根据需要动态地申请和释放内存。

特点

  • 手动管理:堆空间的分配和释放需要程序员手动通过 malloc、calloc、realloc 等函数申请,通过 free 函数释放
  • 大小不固定:堆空间的大小由操作系统管理,理论上可以很大,但受限于物理内存和操作系统的限制。
  • 速度相对较慢:堆空间的分配和释放需要更多的时间来管理内存,且访问速度可能不如栈空间快,因为堆空间中的内存块可能不连续

示例

#include <stdlib.h>  
  
int main() {  
    int *ptr = (int*)malloc(sizeof(int)); // 动态申请一个int大小的堆空间  
    if (ptr != NULL) {  
        *ptr = 10; // 使用这块内存  
        free(ptr); // 释放这块内存  
        ptr=NULL;  //防止野指针
    }  
    return 0;  
}

栈空间:自动管理,速度快,大小有限,适合存储局部变量和函数参数等。

堆空间:手动管理,大小不固定,速度相对较慢,适合需要动态分配大量内存的场景。

8.3 栈与堆的数据结构特点

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

栈是由计算机系统直接提供支持的数据结构,在底层会分配专门的寄存器来存放栈的地址,并且压栈和出栈操作都有专门的指令来执行。正因如此,栈的效率相对较高。而堆则是由 C/C++ 函数库所提供的数据结构,其机制颇为复杂。例如,当要分配一块内存时,库函数会依据特定的算法(具体算法可参考相关的数据结构和操作系统类书籍)在堆内存中搜寻可用且大小足够的空间。若没有足够大小的空间(或许是由于内存碎片过多),那么就可能会调用系统功能来增加程序数据段的内存空间,从而争取分配到足够大小的内存,然后返回。显然,堆的效率要远远低于栈。(这段了解即可)

8.4 栈/堆时间有效性的差异

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
  
// 这个函数演示了栈上内存的使用  
// 栈内存是自动分配的,在函数执行期间有效,函数返回时栈内存通常会被释放(但返回指向栈内存的指针是危险的)  
char* print_stack()  
{  
    char c[100]="I am print_stack func"; // c 是一个局部变量,存储在栈上  
    char *p;  
    p=c; // p 指向 c 的首地址  
    puts(p); // 打印 c 的内容  
    return p; // 返回指向栈上局部变量的指针,这是不安全的,因为栈内存可能在函数返回后被覆盖  
}  
  
// 这个函数演示了堆上内存的使用  
// 堆内存需要手动分配和释放,使用 malloc 和 free  
char *print_malloc()  
{  
    char *p=(char*)malloc(100); // 使用 malloc 在堆上分配 100 字节的内存,并将指针赋给 p  
    // 堆空间在整个进程的生命周期内有效,直到显式调用 free 释放  
    strcpy(p,"I am print malloc func"); // 使用 strcpy 将字符串复制到 p 指向的堆内存中  
    puts(p); // 打印堆内存中的字符串  
    return p; // 返回指向堆上内存的指针,这是安全的,但需要注意后续需要 free  
}  
  
int main() {  
    char *p;  
    p=print_stack(); // 调用 print_stack,p 指向栈上的内存(但已经不安全,因为栈可能已经变化)  
    puts(p); // 这里的行为是未定义的,因为 p 可能不再指向有效的内存  
      
    p=print_malloc(); // 调用 print_malloc,p 指向堆上的内存  
    puts(p); // 打印堆内存中的字符串  

    free(p); // 显式释放堆内存,防止内存泄漏  
    p=NULL; // 防止野指针
      
    // 注意:不要再次使用 p 指向的内存,因为它已经被释放了  
    return 0;  
}  

注意:  
1. 在 print_stack 中返回指向栈内存的指针是不安全的,因为栈内存可能在函数返回后被覆盖。  
2. 在 main 函数中,调用 print_stack 后立即使用返回的指针是未定义行为,因为栈内存可能已经被破坏。  
3. 使用 malloc 分配的内存需要在使用完毕后通过 free 释放,以避免内存泄漏。

4. 在调用 free 之后,不应该再访问 p 指向的内存块,因为它现在可能已经被操作系统重新分配给其他程序使用。为了避免潜在的错误,一个好的习惯是将 p 设置为 NULL,表示它不再指向任何有效的内存块。

输出结果如下所示:

执行结果中为什么第二次打印会有异常?

原因是 print_stack() 函数中的字符串存放在栈空间中,函数执行结束后,栈空间会被释放,字符数组 c 的原有空间已被分配给其他函数使用,因此在调用 print_stack() 函数后, printf("p=%s\n",p);中的 p 不能获取栈空间的数据。而 print_malloc() 函数中的字符串存放在堆空间中,堆空间只有在执行 free 操作后才会释放,否则在进程执行过程中会一直有效。


9 相关重要概念

9.1 野指针

定义:

野指针(Wild Pointer)是指那些指向了非法内存地址的指针这些指针通常是因为未被初始化或者已经被释放(如使用 free 或 delete 后)而仍然被使用的指针。野指针指向的内存区域可能是不可访问的,或者已经被其他程序占用,因此使用野指针进行操作可能会导致程序崩溃、数据损坏或安全漏洞。

避免方法:

  • 初始化所有指针变量,在声明时就赋予一个明确的值(如 NULL或其他明确值)。
  • 在释放指针指向的内存后,立即将指针置为 NULL,防止再次使用。
  • 使用指针前检查其是否为 NULL。
#include <stdio.h>
#include <stdlib.h>

int main() {
    char *ptr; // 声明了一个指针变量ptr,但未初始化,此时ptr是一个野指针

    // 错误的使用方式:直接解引用野指针
    // *ptr = 'a'; // 这将导致未定义行为,因为ptr指向一个未知的内存地址

    // 正确的做法:先初始化指针
    ptr = (char *)malloc(sizeof(char)); // 分配内存并初始化ptr
    if (ptr != NULL) {
        *ptr = 'a'; // 现在ptr指向了一个有效的内存地址,可以安全地解引用
        printf("%c\n", *ptr); // 输出: a

        // 使用完毕后释放内存
        free(ptr);

        // 将ptr置为NULL,防止成为野指针
        ptr = NULL;

        // 尝试解引用ptr(free释放后 ptr 已置为NULL),但先进行NULL检查
        if (ptr != NULL) {
            printf("ptr is not NULL and points to: %c\n", *ptr);
        } else {
            printf("ptr is NULL, safe to avoid dereference\n");
        }
    } else {
        printf("Memory allocation failed\n");
    }

    // 错误的使用方式:尝试解引用已经被释放的指针(未置为NULL)
    // 注意:这里为了演示野指针的危害,故意省略了将ptr置为NULL的步骤
    // *ptr = 'b'; // 如果前面的ptr没有被置为NULL,这里将再次尝试解引用一个野指针

    return 0;
}

注意:在实际编程中,应该始终确保在释放指针后将其置为NULL,并且在使用指针之前检查它是否为NULL。 

9.2 悬垂指针

定义:

悬垂指针(Dangling Pointer)是指那些指向已经被释放的内存块的指针。当使用 free 或 delete 释放了内存后,如果指针没有被设置为 NULL,那么它就变成了悬垂指针。悬垂指针的危险性在于,它看起来仍然是一个有效的指针,但实际上它所指向的内存已经被释放,再次访问这块内存将导致未定义行为。

避免方法:

  • 在释放内存后立即将指针设置为 NULL。
  • 在使用指针之前,总是检查它是否为 NULL。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    char *ptr;

    // 分配内存  
    ptr = (char *)malloc(sizeof(char) * 10); // 分配10个字符的空间  
    if (ptr != NULL) {
        strcpy(ptr, "Hello"); // 拷贝字符串到分配的内存中  
        printf("String: %s\n", ptr); // 输出: String: Hello  

        // 释放内存,但未将ptr置为NULL  
        free(ptr);

        // 此时ptr变成了悬垂指针  
        // 错误的做法:尝试再次使用悬垂指针  
        // printf("Another use: %s\n", ptr); // 这将导致未定义行为  

        // 正确的做法:在释放内存后立即将ptr置为NULL  
        ptr = NULL;

        // 检查ptr是否为NULL,避免使用悬垂指针  
        if (ptr != NULL) {
            printf("ptr is not NULL and points to: %s\n", ptr); // 这行不会执行  
        } else {
            printf("ptr is NULL, safe to avoid dangling pointer\n"); // 输出这个  
        }

        // 注意:即使ptr为NULL,也不要尝试解引用它(如*ptr)  
        // *ptr = 'a'; // 这是错误的,会导致运行时错误  
    } else {
        printf("Memory allocation failed\n");
    }

    return 0;
}

9.3 空指针解引用

定义:

空指针解引用(Dereferencing a Null Pointer)是指尝试访问或修改一个值为 NULL 的指针所指向的内存区域。在 C 语言中,NULL 通常被定义为 (void*)0,表示一个空指针不指向任何有效的内存地址。解引用一个空指针会导致程序崩溃,因为操作系统不允许程序访问地址 0。

避免方法:

  • 在解引用指针之前,始终检查它是否为 NULL。
  • 使用指针时确保它们已被正确初始化并指向有效的内存地址。
#include <stdio.h>  
  
int main() {  
    int *ptr = NULL; // 声明一个空指针  
  
    // 错误的行为:尝试解引用空指针  
    // 这将导致未定义行为,通常表现为程序崩溃  
    // printf("%d\n", *ptr); // 取消注释此行将导致错误  
  
    // 正确的做法:在解引用之前检查指针是否为NULL  
    if (ptr != NULL) {  
        printf("%d\n", *ptr); // 如果ptr不是NULL,则解引用。但在这个例子中,它永远不会执行  
    } else {  
        printf("ptr is NULL, cannot dereference.\n"); // 输出这个  
    }  
  
    // 为了演示如何安全地使用指针,我们可以分配内存并初始化它  
    ptr = (int *)malloc(sizeof(int)); // 分配内存  
    if (ptr != NULL) {  
        *ptr = 10; // 现在ptr指向有效的内存,可以安全地解引用  
        printf("After allocation and assignment: %d\n", *ptr); // 输出: 10  
  
        // 使用完毕后释放内存  
        free(ptr);  
  
        // 将ptr置为NULL,防止成为悬垂指针  
        ptr = NULL;  
  
        // 再次检查ptr是否为NULL  
        if (ptr != NULL) {  
            printf("This will not execute because ptr is NULL.\n");  
        } else {  
            printf("ptr is NULL after freeing memory.\n"); // 输出这个  
        }  
    } else {  
        printf("Memory allocation failed.\n");  
    }  
  
    return 0;  
}

9.4 指针越界

定义:

指针越界(Pointer Bounds Violation)是指指针访问了其分配的内存块之外的内存区域。这通常发生在数组访问中,当索引超出数组边界时,就会访问到不属于该数组的内存区域。指针越界可能导致数据损坏、程序崩溃或安全漏洞。

避免方法:

  • 在使用指针访问数组时,确保索引值在有效范围内。
  • 使用安全的函数或库来管理动态数组和字符串,如 C 标准库中的 strncpy、strncat 等。
  • 在编写代码时考虑使用现代 C++ 的特性(如果你使用的是C++),如智能指针和容器类,它们可以自动管理内存并减少指针越界的风险。
#include <stdio.h>  
  
int main() {  
    int arr[5] = {1, 2, 3, 4, 5}; // 声明并初始化一个整型数组  
    int *ptr = arr; // 指针指向数组的首元素  
  
    // 正确的访问  
    printf("Element 0: %d\n", *(ptr + 0)); // 输出: 1  
    printf("Element 4: %d\n", *(ptr + 4)); // 输出: 5  
  
    // 指针越界的错误访问  
    printf("Element 5 (out of bounds): %d\n", *(ptr + 5)); // 尝试访问不存在的第六个元素  
    // 注意:上面的代码可能导致未定义行为,包括程序崩溃或数据损坏  
  
    // 避免指针越界的一种方法:检查索引是否在有效范围内  
    int index = 5;  
    if (index >= 0 && index < 5) { // 假设我们知道数组的大小是5  
        printf("Element %d: %d\n", index, *(ptr + index));  
    } else {  
        printf("Index %d is out of bounds.\n", index);  
    }  
  
    // 使用库函数避免字符串越界  
    char str[10] = "Hello";  
    char dest[10];  
    // 使用strncpy而不是strcpy来避免目标缓冲区溢出  
    strncpy(dest, str, sizeof(dest) - 1); // 确保不会超出dest的大小  
    dest[sizeof(dest) - 1] = '\0'; // 确保字符串以null结尾  
  
    printf("Safe string copy: %s\n", dest);  
  
    return 0;  
}

10 本章判断题

1、printf( "%d",i);        scanf("%d",&i);是直接访问?

A.正确        B.错误

答案:A

解释:直接访问是指我们通过变量名访问变量值,或者直接拿到变量的地址空间进行访问。


2、int i = 5; int* p=&i; printf("*p=%d\n",*p); 这样去访问 i 的值的方式是间接访问?

A.正确        B.错误

答案:A

解释:我们先获取指针变量 p 的值,*p相当于通过 p 内部所存的地址值,对所在地址进行取值操作,因此 *p 是间接访问。


3、“&” 和 “*” 两个运算符的优先级别相同,但要按自右向左的方向结合,& 是取值,* 是取地址?

A.正确        B.错误

答案:B

解释:“&” 和 “*”两个运算符的优先级别相同是正确的,但是 & 是取地址,* 代表取值,这两个的意义一定要记清楚。


4、int *a,b,c 定义了3个整型指针变量,分别是 a,b,c

A.正确        B.错误

答案:B

解释:a 是指针变量, b 和 c 是整型变量,如果要定义3个指针变量,方法是 int *a,*b,*c;


5、int a; 如果我们编写 *&a 这样的表达式没有意义,因为其与 a 等价的?

A.正确        B.错误

答案:A

解释:* 也就是取值运算符,& 也就是取地址运算符,优先级别相同自右向左的方向结合,因此*&a 这样的表达式没有意义,与 a 等价。


6、本文提到的 main 函数和 change 函数有各自的函数栈空间?

A. 正确        B. 错误

答案: A

解释: 不同的函数有自己独有的栈空间。


7、函数调用是值传递, 将实参的值传递给形参?

A. 正确        B. 错误

答案: A

解释: 这个非常重要,需要记住,因为理解这个,才能明白视频中代码实例的原理,同时研究生复试时,导师可能提问函数调用的原理。


8、虽然函数调用是值传递,但是我们可以通过指针间接访问的原理,来实现子函数中改变 main 函数中某个变量的值?

A. 正确        B. 错误

答案: A

解释: 我们将变量 i 的地址传递给 change 函数时,实际效果是 j=&i,依然是值传递,只是这时我们的 j 是一个指针变量,内部存储的是变量 i 的地址,所以通过 *j 就间接访问到了与变量 i 相同的区域,通过 *j=5 就实现了对变量 i 的值的改变,不太理解的同学需要详细再看一下视频,该小节视频对于理解指针至关重要。 


9、指针做加减就是指指针的偏移?

A. 正确        B. 错误

答案: A

解释: 指针是一个地址,我们会对其做加减运算。


10、我们会对指针变量进行乘除,得到一个新地址来使用?

A. 正确        B. 错误

答案: B

解释: 指针变量存储的是一个内存地址,我们只会对其做加减操作,访问其后面,或者前面空间的内容,不会对其做乘除操作。


11、int a[N]={1,2,3,4,5}; int *p; p=a; 对于这个操作,我们如果进行 p=p+1, p 的值增大了一个字节?

A. 正确        B. 错误

答案: B

解释: 对于指针变量做加法运算时,每次加 1,增加的长度是其基类型的长度,因为基类型是 int,所以 p 的值增大了 4 个字节。


12、数组名传递给子函数时,是弱化为指针的?

A. 正确        B. 错误

答案: A

解释: C 语言的子函数形参位置,并没有数组变量的设计,我们把数组名实参传递给子函数时,形参接收到的是数组名内存储的数组的起始地址。


13、 p=(char*)malloc(20); 代表在堆空间上,申请 20 个字节,由于 malloc 返回的是 void*类型指针,我们要往内部存储字符串,所以强转为 char* ?

A. 正确         B. 错误

答案: A

解释: malloc 是用于申请堆内存空间的,非常重要,一定要掌握,我们给 malloc 传递的值为多大,就是申请多少字节。


14、malloc 申请的空间,如果我们不使用时,一定要 free,否则会一直占用内存空间,直到进程结束?

A. 正确         B. 错误

答案: A

解释: 这个需要记住,因为在中级阶段,数据结构部分,我们不用的空间一定要 free,避免初试扣分。


15、子函数结束后,其栈空间会被释放?

A. 正确         B. 错误

答案: A

解释: 函数执行结束后,其函数栈空间会被全部释放。


11 OJ 练习

11.1 课时6作业1

#include <stdio.h>  
  
// 定义一个函数,该函数接收一个指向整数的指针,并通过解引用该指针来修改其所指向的整数值,将其除以2  
void change(int *i_pointer) {  
    *i_pointer /= 2; // 解引用指针,将其指向的整数值除以2  
}  
  
int main() {  
    int i; // 声明一个整数变量i  
    scanf("%d", &i); // 从标准输入读取一个整数,并将其存储在变量i中  
  
    // 调用change函数,并传递变量i的地址作为参数。这样,change函数就能通过指针修改i的值  
    change(&i);  
  
    // 打印修改后的i的值  
    printf("%d", i);  
  
    return 0; // 程序正常结束  
}

 或者使用指针:

#include <stdio.h>  
  
// 定义一个函数,该函数通过指针修改它所指向的整数值,将其除以2  
void change(int *i_pointer) {  
    *i_pointer /= 2; // 解引用指针,并将指针所指向的整数值除以2  
}  
  
int main() {  
    int i; // 声明一个整数变量i  
    int *i_pointer = &i; // 声明一个指向整数的指针i_pointer,并将其初始化为指向变量i  
      
    // 从标准输入读取一个整数,并将其存储在变量i中  
    scanf("%d", &i);   
      
    // 调用change函数,并传递i_pointer作为参数。这样,change函数就能通过指针修改i的值  
    change(i_pointer);  
      
    // 打印出修改后的i的值  
    printf("%d", i);  
      
    return 0; // 程序正常结束  
}

需要注意的是,不要写成了下面这样:

int *i_pointer;  // 野指针!!!
scanf("%d",i_pointer); // i_pointer 没有指向任何有效的内存地址!!!

change(i_pointer);
printf("%d",*i_pointer);

11.2 课时6作业2

#include <stdio.h>
#include <stdlib.h>

int main() {
    int size;
    scanf("%d", &size);
    // 读取并丢弃一个字符,这是为了去除前一个 scanf 留下的换行符
    // 注意:这种方法在某些情况下可能不足够健壮,但它在这里应该足够
    char c;
    scanf("%c", &c);

    // 使用malloc动态分配足够的内存来存储size个字符的字符串(但注意,这里没有为终止的空字符分配空间)  
    // 正确的做法应该是malloc(size + 1),但这里保持原样以符合题目
    char *strPtr = (char*)malloc(size);

/*    if (strPtr == NULL) {
        // 如果内存分配失败,则打印错误消息并退出程序  
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }*/

    // 使用fgets从标准输入读取最多size-1个字符的字符串(为了留出空间给终止的空字符,但这里没有分配)  
    // 注意:由于strPtr的大小是size,而fgets需要为终止的空字符留出空间,这可能会导致潜在的缓冲区溢出  
    // 正确的做法应该是fgets(strPtr, size + 1, stdin)  ,单位了符合题目
    if (fgets(strPtr, size, stdin) != NULL) {
        // 如果fgets成功读取了字符串,则使用puts将其输出到标准输出  
        // 注意:如果字符串中包含了换行符,它也会被输出  
        puts(strPtr);
    }

    // 释放之前通过malloc分配的内存  
    free(strPtr);

    // 将指针设置为NULL,这是一个好习惯,可以防止野指针问题  
    strPtr = NULL;

    return 0;
} 
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thanks_ks

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值