C语言基础进阶——指针详解

 摘要:本博客将深入讲解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 个元素。

指针是一个变量,存储了一个内存地址,可以指向某个变量或数据结构的位置。

数组是一组相同类型的元素按照一定顺序排列的集合,可以通过下标访问和操作数组元素。

C语言基础进阶——指针详解_第1张图片

数组名与指针的区别

 数组名是常量,不能被赋值。例如,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()函数的使用

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 可以放在 * 前面表示指针指向的对象是常量,或者放在 * 后面表示指针本身是常量。

示例代码:

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() 函数释放动态分配的内存。

指针算术和越界访问:在使用指针进行算术运算时,要确保不越界访问数组或指向无效内存。

指针的生命周期:要确保指针指向的对象在指针使用期间保持有效,避免指针悬挂的问题

字符串操作:使用指针可以对字符串进行操作,例如拷贝、连接、比较等。

多维数组和矩阵:通过指针可以方便地处理多维数组和矩阵,以及进行动态分配和释放。

你可能感兴趣的:(c语言,开发语言)