系统编程(指针,内存基础)
指针复习
指针区分
指针变量
- 指针变量是一种特殊的变量,它存储的是另一个变量的内存地址,而不是数据值本身。
- 在C/C++中,指针变量的声明方式通常是在变量类型前加上一个星号(
*
),例如:int *ptr;
,这里ptr
是一个指向int
类型数据的指针变量。 - 指针变量可以通过取地址运算符(
&
)来存储另一个变量的内存地址。例如:int a = 5; int *ptr = &a;
,这里ptr
存储了变量a
的内存地址。 - 通过指针变量访问它所指向的数据值,需要使用解引用运算符(
*
)。例如:*ptr
将访问ptr
所指向的内存地址中的数据值。 - 指针可以进行一些特定的运算,例如指针加减运算(用于遍历数组或动态内存分配)、指针比较运算(比较两个指针是否指向相同的内存地址)等。
- 一个未指向任何有效内存地址的指针被称为空指针,通常用
NULL
(在C++11及以后标准中,推荐使用nullptr
)来表示。例如:int *ptr = NULL;
。 - 指针常用于动态内存分配,通过标准库函数如
malloc
(在C中)或new
(在C++中)来分配内存,并通过指针来访问这块内存。
int *p;
double a = 10;
int b = 11;
变量指针
变量指针在编程中有多种用途,包括但不限于:
- 动态内存管理:通过指针,我们可以动态地分配和释放内存,以满足程序运行时的内存需求。
- 数组和字符串处理:指针在数组和字符串处理中发挥着重要作用,通过指针可以方便地遍历和修改数组或字符串的元素。
- 函数参数传递:在C/C++中,函数参数默认是按值传递的。但是,通过使用指针作为函数参数,我们可以实现按引用传递,从而允许函数修改传入的变量值。
- 数据结构实现:指针是实现链表、树、图等复杂数据结构的关键工具。通过指针,我们可以建立元素之间的关联关系,从而实现数据的高效访问和操作
p = &b;
数组指针
数组指针本质上是一个指针,但它与普通指针的区别在于它指向的是一个数组的首元素地址,并且它隐含了数组的大小信息(尽管在某些情况下,这个大小信息可能不是显式给出的,但程序员在使用时需要知道或假设它)。
在C语言中,数组指针的定义通常使用如下形式:
类型 (*指针变量名)[数组大小];
数组指针的用途
- 访问二维数组:数组指针在访问二维数组时非常有用。由于二维数组在内存中实际上是按一维数组的方式连续存储的,因此可以使用数组指针来遍历和访问二维数组的元素。
- 函数参数传递:当需要将一个二维数组作为函数参数传递时,可以使用数组指针。这样可以避免在函数内部创建数组的副本,从而提高效率。
- 动态内存分配:虽然数组指针本身并不直接用于动态内存分配,但可以使用指针和动态内存分配函数(如
malloc
)来创建一个动态数组,并使用数组指针来访问这个动态数组。
// 数组指针和二维数组等效
// 指针数组和二级指针等效---形参和实参数据类型对应的角度
int(*p3)[2];
int arr6[2] = {0};
p3 = &arr6;
int arr7[3][2] = {{1,2}, {4,5}, {6, 7}};
p3 = arr7;
指针数组
指针数组本身是一个数组,但其元素都是指针变量。这些指针变量可以指向相同类型的数据,也可以指向不同类型的数据(但在实际使用中,为了操作的便利性和安全性,通常指向相同类型的数据)。指针数组的定义形式为:“类型名 *数组标识符[数组长度]”。例如,int *ptr_array[5]
表示定义了一个包含5个整型指针的数组。
指针数组的特点
- 元素类型相同:指针数组中的元素都是指针变量,且这些指针变量通常指向相同类型的数据。
- 灵活性:由于指针数组中的元素是指针,因此可以通过这些指针灵活地访问和操作数据。
- 节省内存:在处理字符串等变长数据时,使用指针数组可以节省内存空间,因为不需要为每个字符串分配相同长度的字符数组。
指针数组的应用
- 指向字符串:指针数组常用于指向多个字符串,这样可以使字符串处理更加方便和灵活。例如,可以使用指针数组来存储一个字符串数组,并通过指针来访问和修改这些字符串。
- 作为函数参数:指针数组可以作为函数的参数传递,这样函数可以接收一个指向多个数据的指针数组,并对其进行操作。
- 处理二维数组:虽然指针数组和数组指针都可以用于处理二维数组,但指针数组在处理行长度不固定的二维数组时更具优势。通过为每个行分配一个指针,并让这些指针指向相应的数据行,可以灵活地处理不同行长度的二维数组。
指针数组的应用
- 指向字符串:指针数组常用于指向多个字符串,这样可以使字符串处理更加方便和灵活。例如,可以使用指针数组来存储一个字符串数组,并通过指针来访问和修改这些字符串。
- 作为函数参数:指针数组可以作为函数的参数传递,这样函数可以接收一个指向多个数据的指针数组,并对其进行操作。
- 处理二维数组:虽然指针数组和数组指针都可以用于处理二维数组,但指针数组在处理行长度不固定的二维数组时更具优势。通过为每个行分配一个指针,并让这些指针指向相应的数据行,可以灵活地处理不同行长度的二维数组。
#include <stdio.h>
int main() {
char *str_array[3] = {"Hello", "World", "!"}; // 定义一个包含3个字符指针的数组
// 使用指针数组访问和打印字符串
for (int i = 0; i < 3; i++) {
printf("%s ", str_array[i]);
}
printf("\n");
return 0;
}
函数指针
函数指针的定义
函数指针本质上是一个指针,但它与普通指针的区别在于它指向的是一个函数的入口地址,而不是一个变量的地址。函数指针的定义形式为:“返回类型 (*指针变量名)(参数列表)”。这里的“返回类型”指的是函数返回值的类型,“指针变量名”是函数指针的名称,“参数列表”是函数参数的类型列表。
例如,定义一个指向返回整型值、接受两个整型参数的函数的指针,可以这样写:
int (*func_ptr)(int, int);
这里的func_ptr
就是一个函数指针,它可以指向任何返回整型值、接受两个整型参数的函数。
函数指针的赋值
给函数指针赋值时,可以将其指向一个具体的函数。在C语言中,函数名实际上就是该函数的地址,因此可以直接使用函数名来给函数指针赋值。例如:
int add(int a, int b)
{
return a + b;
}
func_ptr = add; // 将add函数的地址赋给func_ptr
函数指针的调用
通过函数指针调用函数时,需要先对指针进行解引用,然后传递相应的参数。例如:
int result = (*func_ptr)(3, 4); // 调用add(3, 4),result的值为7
或者,由于函数指针的特殊性,也可以省略解引用的步骤,直接这样调用:
int result = func_ptr(3, 4); // 同样调用add(3, 4),result的值为7
函数指针的用途
- 回调函数:函数指针常用于实现回调函数。回调函数是指将一个函数作为参数传递给另一个函数,并在适当的时候调用前者。这种机制使得程序更加灵活和可扩展。
- 动态函数调用:通过函数指针,可以在运行时根据需要动态地选择调用哪个函数,从而实现动态函数调用。
- 函数表:可以将多个函数指针存储在一个数组中,形成一个函数表。通过索引访问函数表中的函数指针,可以实现不同的功能调用。
int fun9(const void* a, const void* b)
{
puts("fun9");
return *(double*)b-*(double*)a;
}
//定义函数指针变量p
int(*p5)(const void*, const void *);
p5 = fun9;
//函数回调
qsort(arr10, 5, sizeof(double), fun9);
for(int i = 0; i<5; i++)
{
printf("%lf, ", arr10[i]);
}
puts("");
#include <stdio.h>
// 定义一个函数指针类型,用于指向接受两个int参数并返回int结果的函数
typedef int (*Operation)(int, int);
// 实现加法运算的函数
int add(int a, int b) {
return a + b;
}
// 实现减法运算的函数
int subtract(int a, int b) {
return a - b;
}
// 实现乘法运算的函数
int multiply(int a, int b) {
return a * b;
}
// 实现除法运算的函数(注意处理除以零的情况)
int divide(int a, int b) {
if (b == 0) {
printf("Error: Division by zero!\n");
return 0; // 或者可以选择返回一个特殊的错误代码
}
return a / b;
}
// 定义一个执行运算的函数,它接受一个操作(函数指针)和两个操作数
void performOperation(Operation op, int num1, int num2) {
int result = op(num1, num2);
printf("Result: %d\n", result);
}
int main() {
int num1 = 10;
int num2 = 5;
// 使用加法运算
performOperation(add, num1, num2);
// 使用减法运算
performOperation(subtract, num1, num2);
// 使用乘法运算
performOperation(multiply, num1, num2);
// 使用除法运算
performOperation(divide, num1, num2);
// 尝试除以零(演示错误处理)
performOperation(divide, num1, 0);
return 0;
}
在这个例子中,Operation
是一个函数指针类型,它指向接受两个 int
参数并返回 int
结果的函数。add
、subtract
、multiply
和 divide
是实现了四种基本运算的函数。performOperation
函数接受一个 Operation
类型的函数指针和两个整数作为参数,并使用提供的函数指针来执行运算。
在 main
函数中,我们演示了如何使用 performOperation
函数来执行不同的运算。通过传递不同的函数指针(add
、subtract
、multiply
和 divide
),我们可以灵活地改变 performOperation
函数的行为。
存储方式
代码段(Code Segment)
- 定义:代码段是程序的只读部分,存储的是程序的指令,即代码。
- 特点:代码段是静态的,在程序运行期间不会变化。它是只读的,防止运行时修改代码。每个程序有且只有一个代码段。
- 存储内容:包含所有的函数实现(包括
main
函数和其他用户定义的函数)和常量(如字符串常量)。
数据段(Data Segment)
- 定义:数据段是用来存储全局变量和静态变量的区域。
- 细分:数据段通常被进一步细分为已初始化的数据段(Initialized Data Segment)和未初始化的数据段(BSS Segment,Block Started by Symbol)。
- 已初始化的数据段:存放程序中初始化的全局变量和静态变量。
- 未初始化的数据段(BSS段):存放未初始化的全局变量和静态变量,编译器会自动将这些变量初始化为零。
- 特点:数据段的变量在程序执行期间一直存在,并且可以被多个函数访问和修改。
堆区(Heap)
- 定义:堆区是程序运行时可以动态分配内存的区域。
- 特点:堆内存由程序员显式管理,程序员负责分配(如使用
malloc
函数)和释放(如使用free
函数)内存。堆的内存是动态分配的,大小可变,且在程序运行期间可以随时分配和释放。 - 问题:若忘记释放内存可能导致内存泄漏。
栈区(Stack)
- 定义:栈区是程序在执行过程中用于存放函数的局部变量、函数调用的参数、返回地址等的内存区域。
- 特点:栈由操作系统自动管理,局部变量和函数调用信息会随着函数调用入栈,函数结束后出栈。栈内存是自动管理的(由编译器和操作系统),无需程序员显式分配和释放。栈内存通常比较小,并且是后进先出(LIFO, Last In First Out)的结构。
- 问题:栈的大小是有限的,若超过这个大小会导致栈溢出(Stack Overflow)。
各区域之间的关系
- 代码段与其他区域:代码段存储的是程序的指令,这些指令在运行时可能会访问数据段、堆区或栈区的变量。
- 数据段与堆区、栈区:数据段存储的是全局变量和静态变量,这些变量在程序执行期间一直存在。而堆区和栈区则用于存储程序运行时动态分配的内存。堆区由程序员管理,适合存储需要长时间存在的数据;栈区由操作系统管理,适合存储函数调用的局部变量和参数。
- 堆区与栈区:它们都是动态内存区域,但管理方式不同。堆区需要程序员显式分配和释放内存,而栈区则由操作系统自动管理。此外,栈区内存通常较小且是LIFO结构,适合存储短期使用的数据;堆区内存大小可变,适合存储长期使用的数据。