目录
一、指针变量和地址
C 语言中的指针变量和地址是非常重要的概念,它们在程序中起着至关重要的作用。
地址:
在 C 语言中,每个变量都有一个在内存中的地址,表示该变量存储的位置。可以通过使用取地址操作符 &
来获取变量的地址。例如,&x
表示变量 x
的地址。
指针变量:
指针是指向内存中某个位置的变量。与其他变量存储数据值不同,指针变量存储的是一个地址。可以通过在变量名前加上 *
来声明指针变量,如 int *ptr;
。这里的 ptr
就是一个指向整数类型的指针变量。
指针变量可以指向任何数据类型,例如整数、字符、数组或其他指针等。指针变量存储的是变量的地址,而不是变量本身的值。
使用指针变量:
通过将地址分配给指针变量,我们可以使用指针来访问和修改内存中存储的值。以下是一些常见的指针操作:
- 取地址:使用
&
运算符获取变量的地址。 - 赋值:将一个地址赋值给一个指针变量,如
ptr = &x;
,其中ptr
是一个指向x
变量的指针。 - 取值:使用
*
运算符获取指针指向的变量的值,例如*ptr
表示获取指针ptr
所指向位置的值。
指针的使用场景包括动态内存分配、数组和字符串操作、传递函数参数等。
二、野指针与指针运算
1.野指针
野指针指的是指向无效内存地址的指针,即指针没有正确地指向有效的对象或资源。野指针通常发生在以下情况下:
-
释放了指针所指向的对象后未将指针置空:当释放了一个指针所指向的堆内存或动态分配的内存块之后,如果没有将指针置空或重新指向其他有效内存地址,那么该指针就成为了野指针。
示例:
int* ptr = (int*)malloc(sizeof(int)); // 分配内存 free(ptr); // 释放内存 *ptr = 10; // ptr 现在是野指针,指向无效的内存地址
-
函数返回局部变量的指针:函数中的局部变量在函数执行完毕后会被销毁,此时返回局部变量的指针就会成为野指针。
示例:
int* getLocalPointer() { int num = 10; return # // 返回局部变量的地址 } int* ptr = getLocalPointer(); // ptr 是野指针,指向已经销毁的局部变量
-
函数调用时指针传递错误:当将指向一个对象的指针传递给一个函数,并在函数内释放了该指针所指向的内存后,原来的指针可能变成野指针。
示例:
void releaseMemory(int* ptr) { free(ptr); } int* numPtr = (int*)malloc(sizeof(int)); releaseMemory(numPtr); // 释放了 numPtr 指向的内存 *numPtr = 10; // numPtr 变成了野指针,指向无效的内存地址
在使用野指针时,由于指针指向的内存可能已经被其他变量或对象使用,可能会导致程序崩溃、数据损坏等严重后果。为了避免野指针问题,应该养成良好的编程习惯,包括在释放指针之后将其置空(ptr = NULL;
),避免返回指向局部变量的指针,以及在进行指针传递时确保正确处理内存的分配和释放。此外,注意不要使用未初始化的指针,它们也可能成为野指针。
2.指针运算
指针加法:
在 C 语言中,指针变量可以进行加法运算,如 ptr + 1
,其结果是指针指向地址的下一个位置。加法的单位是指向的类型大小,例如,如果指针是一个 int
类型的指针,那么 ptr + 1
将增加地址 sizeof(int)
。
指针减法:
指针变量也可进行减法运算,例如 ptr - 1
,这将导致指针指向的地址的上一个位置。与加法一样,减法的单位也是指向类型的大小。
指针关系运算:
指针变量可以进行比较运算。例如,两个指向数组元素的指针可以用 >
,<
,==
或 !=
运算符进行比较。如果两个指针指向的是同一数组,那么可以通过比较两个指针之间的距离(即使用减法运算)来检查它们是否在同一范围内。
指针递增和递减:
指针变量也可以使用递增和递减运算符,如 ++
和 --
,它们将指针指向下一个或上一个元素的位置。即 ++ptr
将指向下一个元素,而 --ptr
将指向上一个元素。
需要注意的是,在修改指针的值之前,必须先检查指针是否为 NULL
(即空指针)。如果指针是空值,则指针不能指向任何位置,因此在访问指针所指向的内存之前,需要确保指针已经指向有效的内存地址。
3.指针的使用和传址调用
传址调用:
在函数调用中可以传递变量的地址,这被称为传址调用。通过传递变量的地址给函数,函数可以直接操作原始变量,而不是对函数参数的副本进行操作。这使得函数能够修改原始变量的值并返回修改后的结果。
一般来说,传址调用可以通过指针或引用来实现。在 C 语言中,通常使用指针来实现传址调用。通过将变量的地址传递给函数,在函数内部就可以通过指针来访问和修改该变量。
示例:
void increment(int *numPtr) {
(*numPtr)++; // 通过指针增加变量的值
}
int main() {
int num = 10;
increment(&num); // 传递变量的地址
printf("%d", num); // 输出 11
return 0;
}
在上述示例中,increment
函数接收一个整型指针作为参数,通过解引用该指针来增加变量值。在 main
函数中,我们将变量 num
的地址传递给 increment
函数,从而实现了传址调用并成功修改了变量的值。
三、使用指针访问数组
当我们使用指针访问数组时,实际上是在利用数组名作为指向数组首元素的指针。这意味着可以通过使用指针对数组元素进行访问、遍历和操作。以下是一些关于指针访问数组的重要概念:
-
指向数组的指针: 数组名可以被解释为指向数组中第一个元素的指针。例如,对于整型数组
int arr[5]
,arr
可以被视为&arr[0]
,即指向数组首元素的指针。 -
使用指针访问数组元素: 可以通过对指针进行解引用来访问数组元素。例如,
*arr
表示数组第一个元素的值,*(arr + 1)
表示数组第二个元素的值。 -
指针递增操作: 通过对指针进行递增操作,可以依次访问数组中的各个元素。例如,
arr++
将指针移动到数组中的下一个元素。 -
指针与下标操作: 可以使用指针和下标操作符相结合来访问数组元素。例如,
ptr[2]
表示数组中下标为 2 的元素。
以下是一个简单示例,演示了指针访问数组的方式:
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr; // 将数组名作为指针赋给ptr
for (int i = 0; i < 5; i++) {
printf("%d ", *ptr); // 访问当前指针指向的数组元素
ptr++; // 指针递增,指向下一个数组元素
}
return 0;
}
在上述示例中,通过将数组名 arr
赋值给指针 ptr
,然后使用指针遍历数组并输出每个元素的值。通过指针访问数组可以提供对数组元素的灵活和高效访问方式,特别是在涉及动态分配和处理数组的情况下。
一维数组传参的本质
当我们将一维数组作为参数传递给函数时,实际上传递的是数组的指针,而不是整个数组的副本。这意味着,在函数内部对传递的数组进行修改时,会直接影响到原始数组。
C 语言中的数组名可以被解释为指向数组首元素的指针。因此,当我们将数组作为参数传递给函数时,实际上传递的是数组名所指向的首元素的地址。函数内部可以通过指针访问和操作数组的元素。这种方式在传递大型数组时比较高效,因为不需要复制整个数组。
以下是一个示例,演示了一维数组作为函数参数的本质:
#include <stdio.h>
void modifyArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
arr[i] += 1; // 修改原始数组的元素值
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
modifyArray(arr, 5); // 传递数组作为参数
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]); // 输出修改后的数组元素
}
return 0;
}
四、指针变量类型
1.字符指针变量
char *str = "Hello, World!"; // 字符指针指向一个字符串常量
char ch = *str; // 解引用字符指针,获取第一个字符
printf("%c\n", ch); // 输出:H
- 字符指针变量是指向字符类型数据的指针。可以指向单个字符或字符串。
- 使用字符指针可以进行字符串的处理和操作,例如字符串的遍历、复制、拼接等。
- 常用的字符串操作函数(如
strcpy
、strcmp
等)通常使用字符指针作为参数。
2.数组指针变量
int arr[5] = {1, 2, 3, 4, 5};
int (*ptr)[5] = &arr; // 数组指针指向一个具有5个整型元素的数组
int val = (*ptr)[2]; // 解引用数组指针,获取第三个元素的值
printf("%d\n", val); // 输出:3
- 数组指针变量是指向数组的指针。可以指向一维或多维数组。
- 使用数组指针可以进行数组元素的访问和操作。通过指针算术运算和解引用操作,可以访问数组中的元素。
- 可以使用数组指针作为函数的形参,从而传递数组给函数。
3.函数指针变量
int add(int a, int b) {
return a + b;
}
int (*ptr)(int, int) = add; // 函数指针指向add函数
int result = ptr(2, 3); // 通过函数指针调用函数
printf("%d\n", result); // 输出:5
- 函数指针变量指向函数的指针。可以将函数地址赋值给函数指针变量。
- 使用函数指针可以在运行时动态地调用不同的函数或将函数作为参数传递给其他函数。
- 函数指针的声明需要与函数原型保持一致,确保指针类型与函数返回值类型和参数类型匹配。
4.二维数组传参的本质
当传递二维数组给函数时,实际上是将数组的地址传递给函数。二维数组在内存中是以一维线性方式存储的,因此传递二维数组本质上就是传递第一个元素(即第一行)的地址。
例如,如果有一个 int 类型的二维数组 arr[3][4]
,则在传递给函数时,实际上传递的是 arr
的地址,也就是 &arr[0]
。
函数的形参声明应该指明列的大小,列数目是可选的,但行数目是必需的。例如:
void func(int arr[][4], int rows) {
// 函数体内可以使用 arr[i][j] 对二维数组进行操作
}
这是因为在传递数组给函数时,编译器需要知道每一行有多少个元素,这样才能正确地进行地址偏移计算,从而访问到正确的数组元素。
五、sizeof和strlen的对比
sizeof
和 strlen
是 C 语言中常用的两个函数,但它们的作用和用法是不同的。
-
sizeof:
sizeof
是一个运算符,而不是函数。它用于计算数据类型或变量所占据的内存空间大小(以字节为单位)。sizeof
可以用于计算任何数据类型的大小,包括基本数据类型、复合数据类型、自定义类型和结构体等。sizeof
在编译时计算,返回的是静态的大小信息。
示例:
int num = 10; int size = sizeof(num); // 计算 int 类型变量 num 的大小 printf("%d\n", size); // 输出:4 int arr[5] = {1, 2, 3, 4, 5}; size = sizeof(arr); // 计算 int 类型数组 arr 的大小 printf("%d\n", size); // 输出:20
-
strlen:
strlen
是一个函数,用于计算字符串的长度,即字符串中字符的数量(不包括字符串末尾的’\0’)。strlen
只能用于计算字符串类型数据的长度。strlen
在运行时遍历字符串的字符,直到遇到字符串末尾的\0
字符才停止,并返回字符的数量。
示例:
char str[] = "Hello"; int length = strlen(str); // 计算字符串 str 的长度 printf("%d\n", length); // 输出:5 char str2[] = {'H', 'e', 'l', 'l', 'o'}; length = strlen(str2); // 错误!字符串没有以'\0'结尾,会导致不确定的结果 const char* str3 = "Hello World"; length = strlen(str3); // 计算字符串常量的长度 printf("%d\n", length); // 输出:11
需要注意的是,sizeof
对数组和指针返回的结果不同。对于数组,sizeof
返回整个数组所占用的内存空间大小;而对于指针,sizeof
返回指针本身的大小(通常是机器字长大小)。