摘要:本博客将深入讲解C语言中的指针概念及其使用方法,帮助读者更好地理解和应用指针。
目录
引言
什么是指针?
指针的作用和优势
指针基础
指针的定义和声明
指针的初始化
指针的运算(地址运算、指针运算)
指针与数组
数组与指针的关系
数组名与指针的区别
指针与二维数组
指针与函数
函数参数传递(值传递、指针传递、引用传递)
函数返回指针
指针作为函数的返回值
动态内存分配
动态内存分配的概念和优势
malloc()函数的使用
内存泄漏和内存释放
指针与结构体
结构体指针的定义和使用
结构体指针作为函数参数和返回值
结构体指针与动态内存分配
常见指针问题与技巧
空指针和野指针
const关键字与指针
指针数组和数组指针
总结
指针是C语言中一种重要的数据类型,它用于存储内存地址。简单来说,指针就是一个变量,但它不直接存储值,而是存储另一个变量的内存地址。
在计算机内存中,每个变量都有一个唯一的内存地址,指针就是用来保存这个地址的变量。通过指针,我们可以直接访问和操作内存中的数据,而不需要知道具体的变量名。如定义一个函数(sep),其功能为进行两个数的交换。
void sep(int m,int n)//定义一个有参无返回值的sep函数,进行两个数的交换
{
int sep;
sep = m;
m = n;
n = sep;
}
上述代码,是初学者在学C时容易犯的一个错误,在函数中声明的形式参数(形参)是局部变量,它的作用域只能在这个函数中,即在这个函数中使用,当函数结束时,其所分配的地址就会被收回,故无法真正实现两个数交换,那么我们要如何操作才能实现两个数的函数功能,单纯的进行值交换是不可行的,那么我们交换这两个数的地址,能否到达目的,答案显然是可以的,故这个时候我们就需要依赖于指针。
故正确改进代码
void sep(int *m, int *n)
{
int sep; // 声明一个整型变量 sep
sep = *m; // 将指针 m 所指向的变量的值赋给 sep
*m = *n; // 将指针 n 所指向的变量的值赋给指针 m 所指向的变量
*n = sep; // 将 sep 的值赋给指针 n 所指向的变量
}
通过指针,我们可以实现以下几个重要的操作:
取址(取得变量的地址):使用取址运算符(&)可以获取变量的内存地址,将其赋值给指针变量。例如:
int num = 10;
int *ptr = # // 将num的地址赋值给指针ptr
解引用(访问指针指向的值):使用解引用运算符(*)可以访问指针指向的值。例如:
int num = 10;
int *ptr = #
printf("%d", *ptr); // 输出指针ptr所指向的值(即num的值)
解引用操作将返回指针所指向的变量的值。
指针的运算:指针可以进行一些运算,如指针的加法、减法、比较等。这些运算通常用于遍历数组、访问动态分配的内存等场景。
指针在C语言中广泛应用于各种场景,如动态内存分配、函数参数传递、数组操作等。掌握指针的概念和使用方法对于理解和编写C语言程序非常重要。
在C语言中,可以使用星号(*)来定义和声明指针变量。语法如下:
数据类型 *指针变量名;
这里,数据类型是指针所指向的变量的类型,指针变量名是你给指针变量起的名称。例如,要声明一个指向整数的指针变量,可以这样写:
int *ptr; // 声明一个指向整数的指针变量ptr
在定义指针变量时,进行赋值,就叫做初始化
int a;
int *pre = &a; //类型 * 指针名 = 地址;即指针初始化
请注意,星号(*)在声明指针变量时放在数据类型前面。
另外,还可以将指针初始化为 NULL
,表示指针不指向任何有效的内存地址。示例如下:
int *ptr = NULL; // 将指针ptr初始化为NULL
在使用指针之前,始终确保对其进行初始化,以避免访问未知的内存地址。
地址运算:使用取址运算符(&)可以获取变量的地址。例如:&num
表示变量 num
的地址。
指针运算:指针变量可以进行一些运算,如加法、减法、比较等。这些运算通常用于遍历数组、访问动态分配的内存等场景。
加法运算:可以对指针进行加法运算,以便在内存中移动到相邻的位置。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 指向数组arr的第一个元素的指针
ptr = ptr + 1; // 指针向后移动一个元素位置(即pre + i == pre + sizeof(指针类型)*i)
减法运算:可以对指针进行减法运算,以计算两个指针之间的距离(以元素为单位)。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr1 = &arr[0]; // 指向数组arr的第一个元素的指针
int *ptr2 = &arr[3]; // 指向数组arr的第四个元素的指针
int distance = ptr2 - ptr1; // 计算两个指针之间的距离(以元素为单位)
比较运算:可以对指针进行比较运算,判断两个指针是否相等或大小关系。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr1 = &arr[0];
int *ptr2 = &arr[2];
if (ptr1 == ptr2) {
// 指针相等
}
if (ptr1 < ptr2) {
// ptr1指向的位置在ptr2之前
}
注意指针也可以使用自增自减运算
int *pre;
pre++ //先取出指针pre的值,再进行加1(加一个指针类型的值)
++pre //先将指针pre进行加1,再进行运算
指针运算在C语言中非常有用,特别是在处理数组和动态内存分配时。
指针与数组是在C语言中密切相关的概念,它们之间有一些关系和区别。
数组与指针的关系
在C语言中,数组名可以被解释为指向数组首元素的指针。也就是说,数组名可以被隐式地转换为指向数组第一个元素的指针。例如,对于数组 int a[5]
,可以将 p 视为指向 a[0]
的指针。
另外,指针也可以用于访问数组元素。可以使用指针算术运算(如指针加法或指针减法)来遍历数组元素。例如,*(p + i)
可以用于访问数组 a
的第 i
个元素。
指针是一个变量,存储了一个内存地址,可以指向某个变量或数据结构的位置。
数组是一组相同类型的元素按照一定顺序排列的集合,可以通过下标访问和操作数组元素。
数组名是常量,不能被赋值。例如,arr = &some_var
是非法的。
指针是变量,可以被赋值。例如,int *ptr = &some_var
是合法的。
此外,数组名作为函数参数时,会被隐式地转换为指向数组首元素的指针。这意味着在函数中对数组的修改会影响到原始数组。
int *p,a[5];
p = a;
p + n == a + n//等价
//等价用法
a[n] == *(p+n) == *(a+n) == p[n]
二维数组:二维数组中,每个元素是一个一维数组,在元素(一维数组)中,每个成员就是一个值
二维数组:
数据类型数组名[行][列]
行:有多少个一维数组
列:一维数组的元素个数
对于二维数组而言,数组名是整个二维数组的首地址,第零个元素的地址,二维数组的元素都是一维数组,即二维数组数组名表示其中元素,整个一维数组的地址。
int a[3][4];//声明一个三行四列的二维数组
a == &a[0];//a[0]是整个一维数组
//由于a表示整个元素的地址(一维数组的地址),所以进行指针运算时,+1 ,加上 整个一维数组大小
//a:&a[0],第零个一维数组的地址
//a+1:&a[1],第一个一维数组的地址
//a+2:&a[2],第二个一维数组的地址
//a[0]:表示二维数组的元素零,第零个一维数组,a[0]是一维数组的数组名,在这个一维数组的首地址
//a[0] == &a[0][0]
//a[0]+1 == &a[0][1]
//a[1]:第一个一维数组,也是这个一维数组的数组名(首地址)
//a[1] == &a[1][0]
//a[1]+1 == &a[1][1]
//注意:
//a+1,表示移动二维数组的一个元素(一维数组)大小
//a[0]+1,表示移动一维数组的一个元素(数据类型值)大小
对于一维数组,可以使用指针来访问和操作数组元素,如上述所述。
对于多维数组,可以使用指针和指针的指针(或称为二级指针)来访问和操作数组元素。多维数组在内存中是按行优先(row-major)或列优先(column-major)的方式存储的,可以使用指针算术运算来遍历多维数组元素。
例如,对于二维数组 int a[3][4]
,可以使用指针和指针的指针来访问和操作数组元素。例如,int *ptr = &a[0][0]
可以将指针 ptr
指向数组的首元素,然后可以使用指针算术运算来遍历二维数组的元素。
值传递:当使用值传递方式将参数传递给函数时,函数会创建参数的副本,并在函数内部使用副本进行操作。对参数的修改不会影响到原始变量。
指针传递:通过指针传递参数时,函数接收指针作为参数,并可以通过指针来访问和修改原始变量的值。这样可以实现在函数内部对原始变量的修改。
引用传递:在C++中引入了引用传递的概念,可以通过引用传递参数。引用传递类似于指针传递,但语法更简洁,可以直接操作原始变量,而无需使用指针解引用。
指针传递:
示例代码:
#include
void changeValue(int *ptr)
{
*ptr = 100; // 修改指针所指向的值
}
int main()
{
int num = 50;
printf("Before: %d\n", num);
changeValue(&num); // 传递指针
printf("After: %d\n", num);
return 0;
}
输出:
Before: 50
After: 100
在这个例子中,changeValue
函数接收一个指向 int
类型的指针 ptr
,通过解引用指针来修改原始变量 num
的值。
在C语言中,函数可以返回指针作为其返回值。这样的函数被称为指针返回函数。
指针返回函数可以用于返回指向动态分配内存的指针,或者返回指向函数内部静态变量的指针。返回的指针可以在函数外部使用,以访问和操作函数内部创建的数据。
示例代码:
#include
int* createArray(int size) {
int* arr = malloc(size * sizeof(int)); // 动态分配内存
for (int i = 0; i < size; i++) {
arr[i] = i + 1; // 初始化数组元素
}
return arr; // 返回指针
}
int main() {
int* ptr = createArray(5); // 接收返回的指针
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]); // 访问指针指向的数组元素
}
free(ptr); // 释放内存
return 0;
}
输出:
1 2 3 4 5
在这个例子中,createArray
函数动态分配了一个大小为 size
的整数数组,并返回指向该数组的指针。在 main
函数中,我们接收返回的指针,并通过指针访问和打印数组的元素。最后,使用 free
函数释放了动态分配的内存。
函数可以返回指针作为其返回值,这样的函数被称为指针返回函数。
指针返回函数可以用于返回指向动态分配内存的指针,或者返回指向函数内部静态变量的指针。返回的指针可以在函数外部使用,以访问和操作函数内部创建的数据。
示例代码:
#include
int* findMax(int* arr, int size) {
int* max = arr;
for (int i = 1; i < size; i++) {
if (arr[i] > *max) {
max = &arr[i];
}
}
return max;
}
int main() {
int numbers[] = {10, 5, 8, 12, 3};
int size = sizeof(numbers) / sizeof(numbers[0]);
int* maxPtr = findMax(numbers, size);
printf("Max value: %d\n", *maxPtr);
return 0;
}
输出:
Max value: 12
在这个例子中,findMax
函数接收一个整数数组和数组的大小,并返回指向数组中最大值的指针。在 main
函数中,我们接收返回的指针,并通过解引用指针来获取最大值。
这些示例代码展示了指针与函数的使用。指针传递允许函数修改原始变量的值,函数返回指针允许在函数外部访问和操作函数内部的数据。请注意,在实际使用中,需要谨慎处理指针的内存分配和释放,以避免内存泄漏和悬空指针的问题。
需要注意的是,在使用指针传递或返回指针的函数时,需要确保指针指向的内存有效,并在适当的时候进行内存的分配和释放,以避免内存泄漏和悬空指针的问题。
栈中变量,只要离开了变量的生命周期,变量就会由系统销毁释放
堆空间:有程序员在程序中,自己进行管理的空间,需要使用时进行申请,由自己进行释放销毁。空间的申请与释放都是由程序员在程序中指定,通过地址指针进行访问空间,根据指针的类型就按照对应类型进行访问,就叫做动态内存
动态内存分配的优势:
灵活性:动态内存分配允许根据程序的需要在运行时分配所需的内存空间。
内存利用率:可以动态地分配所需大小的内存,避免了静态分配可能造成的内存浪费。
数据结构的动态性:动态内存分配使得创建和操作动态数据结构(如链表、树等)变得更容易。
malloc()
函数用于在堆(heap)上分配指定大小的内存块。
它接收一个参数,即所需内存的字节数,并返回一个指向分配内存的指针。
如果分配成功,返回的指针指向连续的、未初始化的内存块。
示例代码:
int* ptr = (int*)malloc(10 * sizeof(int));
内存泄漏是指在动态内存分配后,没有正确释放已分配的内存,导致无法再次使用该内存,从而造成内存资源的浪费。内存泄漏可能会导致程序运行时内存消耗过大,最终导致程序崩溃或性能下降。
内存释放是指在不再需要使用动态分配的内存时,显式地将其释放,以便系统可以重新使用该内存。在C语言中,使用 free()
函数来释放动态分配的内存。例如:
free(ptr);
在释放内存后,应将指针设置为 NULL
,以避免出现悬空指针的问题:
ptr = NULL;
正确管理动态内存分配是编程中的重要问题。确保在不再需要使用动态分配的内存时进行释放,避免内存泄漏,以提高程序的效率和稳定性。
结构体指针是指向结构体变量的指针,可以通过指针来访问和修改结构体的成员。
定义结构体指针时,需要使用结构体类型名称和 *
运算符。
示例代码:
#include
struct Person {
char name[20];
int age;
};
int main() {
struct Person person1;
struct Person* ptr;
ptr = &person1; // 将指针指向结构体变量
// 通过指针访问结构体成员
printf("Enter name: ");
scanf("%s", ptr->name);
printf("Enter age: ");
scanf("%d", &(ptr->age));
// 打印结构体成员
printf("Name: %s\n", ptr->name);
printf("Age: %d\n", ptr->age);
return 0;
}
输出:
Enter name: John
Enter age: 25
Name: John
Age: 25
在这个例子中,我们定义了一个名为 Person
的结构体类型,并创建了一个结构体变量 person1
。然后,我们定义了一个指向 Person
结构体的指针 ptr
,并将其指向 person1
。通过指针访问结构体成员时,使用 ->
运算符。
结构体指针可以作为函数的参数和返回值,以便在函数内部访问和操作结构体。
示例代码:
#include
struct Point {
int x;
int y;
};
//声明一个函数通过指针传入参数进行打印
void printPoint(struct Point* ptr) {
printf("x: %d, y: %d\n", ptr->x, ptr->y);
}
//构造一个结构体
struct Point* createPoint(int x, int y) {
struct Point* ptr = malloc(sizeof(struct Point));
ptr->x = x;
ptr->y = y;
return ptr;
}
int main() {
struct Point* p1 = createPoint(3, 4);
printPoint(p1);
free(p1);
return 0;
}
输出:
x: 3, y: 4
在这个例子中,我们定义了一个名为 Point
的结构体类型,并创建了一个函数 printPoint
来打印结构体的成员。函数 createPoint
动态分配了一个 Point
结构体,并返回指向该结构体的指针。在 main
函数中,我们创建了一个 Point
结构体指针 p1
,并将其传递给 printPoint
函数进行打印。最后,我们使用 free
函数释放了动态分配的内存。
输出:
Width: 5, Height: 3
在这个例子中,我们定义了一个名为 Rectangle
的结构体类型,并创建了一个函数 createRectangle
来动态分配一个 Rectangle
结构体,并返回指向该结构体的指针。在 main
函数中,我们创建了一个 Rectangle
结构体指针 rect
,并通过 createRectangle
函数进行动态内存分配。最后,我们打印结构体的成员,并使用 free
函数释放了动态分配的内存。
结构体指针可以与动态内存分配一起使用,以便在运行时动态创建结构体对象。
示例代码:
#include
#include
struct Rectangle {
int width;
int height;
};
struct Rectangle* createRectangle(int width, int height) {
struct Rectangle* rect = malloc(sizeof(struct Rectangle));
rect->width = width;
rect->height = height;
return rect;
}
int main() {
struct Rectangle* rect = createRectangle(5, 3);
printf("Width: %d, Height: %d\n", rect->width, rect->height);
free(rect);
return 0;
}
结构体指针在C语言中非常有用,可以通过指针来访问和修改结构体的成员,还可以将结构体指针作为函数的参数和返回值,以便在函数内部操作结构体。结合动态内存分配,可以在运行时动态创建和管理结构体对象。记得在不再需要使用动态分配的内存时释放内存,以避免内存泄漏。
空指针是指没有指向任何有效对象或函数的指针。在C语言中,可以使用宏定义 NULL
或直接使用整数常量 0
来表示空指针。
野指针是指指向未知或无效内存地址的指针。野指针可能是未初始化的指针、已释放的指针、或者指向非法内存的指针。使用野指针可能导致程序崩溃或产生不可预测的行为。
在使用指针之前,应始终将其初始化为 NULL
,并在不再使用指针时将其设置为 NULL
,以避免出现野指针的问题。
const关键字用于声明常量,可以应用于指针类型。
const
可以放在 *
前面表示指针指向的对象是常量,或者放在 *
后面表示指针本身是常量。
示例代码:
const int* ptr; // 指向常量的指针
int* const ptr; // 常量指针
const int* const ptr; // 常量指针指向常量
第一个示例中,ptr
是一个指向常量的指针,即不能通过 ptr
修改所指向的值,但可以通过其他方式修改该值。
第二个示例中,ptr
是一个常量指针,即指针本身是常量,不能指向其他内存地址,但可以通过 ptr
修改所指向的值。
第三个示例中,ptr
是一个常量指针指向常量,既不能通过 ptr
修改所指向的值,也不能将 ptr
指向其他内存地址。
指针数组是一个数组,其中的每个元素都是指针。每个指针可以指向不同的对象。
数组指针是一个指针,指向一个数组的首地址。它可以通过指针算术运算来访问数组的元素。
示例代码:
int* arr[5]; // 指针数组
int (*ptr)[5]; // 数组指针
在第一个示例中,arr
是一个包含5个元素的指针数组,每个元素都是指向int类型的指针。
在第二个示例中,ptr
是一个指向包含5个int类型元素的数组的指针。
指针在C语言中是一个强大且常用的工具,但也容易出现一些问题。要注意初始化指针并避免使用野指针,可以使用 NULL
来表示空指针。使用 const
关键字可以声明指向常量的指针或常量指针。指针数组和数组指针是不同的概念,分别表示数组的指针和指针的数组。理解这些概念并正确使用指针可以提高程序的可读性和健壮性。
指针在C语言中具有广泛的应用场景,以下是一些常见的指针应用场景和注意事项:
应用场景:
动态内存分配:使用指针可以在运行时动态地分配和管理内存,例如使用 malloc()
、calloc()
和 realloc()
函数来动态分配内存。
传递大型数据结构:通过传递指针而不是整个数据结构,可以减少函数调用的开销,提高程序的效率。
注意事项:
空指针和野指针:始终将指针初始化为NULL,并在不再使用指针时将其设置为NULL,以避免野指针的问题。
内存泄漏和释放:在动态内存分配后,必须记得释放内存,避免内存泄漏,使用 free()
函数释放动态分配的内存。
指针算术和越界访问:在使用指针进行算术运算时,要确保不越界访问数组或指向无效内存。
指针的生命周期:要确保指针指向的对象在指针使用期间保持有效,避免指针悬挂的问题
字符串操作:使用指针可以对字符串进行操作,例如拷贝、连接、比较等。
多维数组和矩阵:通过指针可以方便地处理多维数组和矩阵,以及进行动态分配和释放。