系列文章目录

第一章 C语言基础知识

第二章 C语言控制语句

第三章 C语言函数详解

第四章 C语言数组详解

第五章 C语言操作符详解

第六章 C语言指针详解

第七章 C语言结构体详解

文章目录

1. 字符指针

2. 指针数组

 3. 数组指针

3.1 语法

3.2 使用示例

3.3 &数组名和数组名

数组名

&数组名

使用区别

3.4 数组指针的使用示例

3.5 综合示例

4. 数组参数、指针参数

4.1 一维数组传参

1. 通过指针传递

2. 通过固定大小数组参数传递

3. 通过可变长度数组参数传递(C99特性)

4.2 二维数组传参

1. 明确列维度的数组参数

2. 使用指针到数组的指针

4.3 一级指针传参

 1. 修改基本数据类型的值

2. 处理数组


1. 字符指针

字符指针是一个指针类型,专门用于指向字符类型数据的地址,通常用来处理字符串和字符数组。

char *str = "Hello, world!";

代码示例:

char s[] = "Hello";
char *p = s;  // p指向s的第一个元素

// 输出字符串
while(*p != '\0') {
    printf("%c", *p);
    p++;
}

// 修改字符串
p = s;  // 重置p指向s的第一个元素
p[1] = 'a';  // 将'e'改为'a'
printf("\nModified string: %s\n", s);

 使用方法一:

int main() {
    char ch = 'w';
    char *pc = &ch; // 指针pc指向字符变量ch
    *pc = 'w';      // 通过指针pc修改ch的值
    return 0;
}
  • 变量定义:定义了一个字符变量ch并初始化为'w'
  • 指针使用:定义了一个指向字符的指针pc,并将其指向ch的地址。接着通过指针pc修改了ch的值。虽然这里重新将ch的值设置为'w',实际上并没有改变任何内容。
  • 内存操作:这个程序操作的是堆栈上的内存,具体是局部变量ch的存储空间。

使用方法二:

int main() {
    const char* pstr = "hello bit."; // pstr是一个指向字符串常量的指针
    printf("%s\n", pstr);
    return 0;
}
  • 这里是把字符串 hello bit. 首字符的地址放到了pstr中。
  • 字符串字面量:这里"hello bit."是一个字符串字面量,存储在程序的只读数据段。
  • 指针定义:定义了一个指向const char的指针pstr。这意味着pstr指向的内容不应被修改,这也符合字符串字面量存放在只读内存区的特性。

两个代码的主要区别:

  1. 内存位置和安全性

    • 第一个程序中的ch存储在栈上,可以安全地通过指针修改它的值。
    • 第二个程序中的pstr指向的是存储在只读内存段的字符串字面量,通过pstr修改字符串将导致运行时错误(通常是程序崩溃)。
  2. const关键字的使用

    • 在第二个程序中,const char*表明指针指向的字符内容不应该被修改,这保护了指向只读内存的字符串字面量。
    • 第一个程序中,没有使用const,因为pc指向的ch是可以修改的。

练习:下面的输出是什么?

#include <stdio.h>
int main()
{
 char str1[] = "hello bit.";
 char str2[] = "hello bit.";
 const char *str3 = "hello bit.";
 const char *str4 = "hello bit.";
 if(str1 ==str2)
 printf("str1 and str2 are same\n");
 else
 printf("str1 and str2 are not same\n");
 
 if(str3 ==str4)
 printf("str3 and str4 are same\n");
 else
 printf("str3 and str4 are not same\n");
 
 return 0;
}

输出为:str1 and str2 are not same;str3 and str4 are same

  • 当比较 str1str2 时,比较的是两个数组的起始地址。由于这两个数组独立分配,它们的地址是不同的,因此结果将显示 str1str2 不相同。
  • 当比较 str3str4 时,比较的是两个指针的值。如果编译器对字符串字面量 "hello bit." 进行了优化,使得所有的指针都指向相同的内存地址,则这两个指针指向的是相同的地址。因此,结果可能显示 str3str4 是相同的。

2. 指针数组

指针数组是一个数组,其元素都是指针。这种类型的数组可以用来存储指向不同数据类型的指针,例如整数、字符、其他数组或者其他指针等。

int* arr1[10]; //整形指针的数组
  • 含义arr1 是一个数组,包含 10 个元素,每个元素都是一个指向 int 类型的指针。这意味着每个数组元素可以存储一个指向整数的内存地址。
  • 用途:这种类型的数组通常用于存储指向不同整数或整数数组的指针。例如,可能有多个动态分配的整数数组,每个数组的长度可以不同,可以使用 arr1 的每个元素来指向这些数组。
char *arr2[4]; //一级字符指针的数组
  • 含义arr2 是一个数组,包含 4 个元素,每个元素都是一个指向 char 类型的指针。这种指针通常用于指向字符串。
  • 用途:每个指针可以指向一个独立的字符串,这使得 arr2 成为一个非常适合存储多个字符串(如多个名字或不同的文本条目)的结构。
char **arr3[5];//二级字符指针的数组
  • 含义arr3 是一个数组,包含 5 个元素,每个元素都是一个指向指向 char 类型的指针的指针(即指针的指针)。
  • 用途:这种类型的数组通常用于更复杂的数据结构,如动态数组的动态数组,或者用于存储指向不同字符串数组的指针。例如可以用 arr3 来指向不同的字符串数组,其中每个数组可能包含不同数量的字符串。这也常见于处理复杂的多维字符串数据,或者在函数中动态改变指向字符串的指针。

 3. 数组指针

数组指针和刚刚的指针数组不一样,它是一种指针类型,用于指向一个完整的数组,而不仅仅是数组的单个元素。与指针数组不同的是,后者是包含多个指针的数组。数组指针能够指向并通过其指针操作整个数组。

3.1 语法

type (*pointerName)[arraySize];
int (*p)[10];

这里,type 是数组元素的数据类型,pointerName 是数组指针的名称,而arraySize 是数组中元素的数量。

p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

3.2 使用示例

int arr[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

int (*ptr)[4] = arr;

ptr 是一个指向包含 4 个整数的数组的指针。arr 是一个 3x4 的二维数组,而ptr 可以用来遍历arr。

遍历二维数组

使用数组指针遍历二维数组的代码如下:

for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 4; j++) {
        printf("%d ", ptr[i][j]); // 访问数组元素
    }
    printf("\n");
}

动态数组与数组指针

数组指针也可以用来指向动态分配的数组。例如,动态分配一个二维数组:

int (*ptr)[4];
ptr = (int (*)[4])malloc(3 * sizeof(*ptr));

// 初始化并打印数组
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 4; j++) {
        ptr[i][j] = i * 4 + j + 1; // 填充数据
        printf("%d ", ptr[i][j]);
    }
    printf("\n");
}

// 释放内存
free(ptr);

3.3 &数组名和数组名

数组名

数组名被视为指向数组首元素的指针。也就是说,它表示数组第一个元素的内存地址。例如,如果有一个数组 int arr[5];,则表达式 arr 在不带有 & 的情况下,转换为一个指向数组第一个元素(arr[0])的指针。

int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;  // p 指向 arr[0]

&数组名

当使用 & 与数组名时,如 &arr,得到的是指向整个数组的指针,这是一种不同类型的指针。这种指针的类型不是指向单个元素,而是指向整个数组,它的类型表示为 类型 (*指针名)[数组大小]。在例子中,&arr 的类型是 int (*)[5]。

  • arr 指向数组的第一个元素,类型为 int*
  • &arr 指向整个数组,类型为 int (*)[5]

使用区别

  • 使用 arr 时可以像使用普通指针一样,通过 arr[i]*(arr + i) 来访问数组元素。
  • 使用 &arr 时必须考虑到它是一个指向整个数组的指针。要访问数组的元素,需要先解引用这个指针,然后指定元素索引,如 (*&arr)[i]

指针运算区别:

  • arr 进行加法运算时(如 arr + 1),结果是移动到下一个元素的地址(即从 arr[0]arr[1])。
  • &arr 进行加法运算时(如 &arr + 1),结果是跳过整个数组到数组后面的地址,因为 &arr 的类型意味着它的步长是整个数组的大小。

代码示例:

#include <stdio.h>
int main()
{
  int arr[10] = {0};
  printf("%p\n", arr);
  printf("%p\n", &arr);
  return 0;
}

C语言指针进阶-LMLPHP

输出将显示 arr&arr 的值相同,但类型不同。这是因为:

  • arr 给出了数组第一个元素的地址。
  • &arr 也给出了数组的起始地址,但它指的是整个数组。
#include <stdio.h>
int main()
{
 int arr[10] = { 0 };
 printf("arr = %p\n", arr);
 printf("&arr= %p\n", &arr);
 printf("arr+1 = %p\n", arr+1);
 printf("&arr+1= %p\n", &arr+1);
 return 0;
}

C语言指针进阶-LMLPHP

  • arr + 1 会跳到下一个整数的地址。
  • &arr + 1 会跳过整个数组的内存布局,到达数组后的第一个位置。

总之,arr 作为指向第一个元素的指针,而 &arr 是指向整个数组的指针。

3.4 数组指针的使用示例

// print_arr1使用一个正常的二维数组形式作为参数
void print_arr1(int arr[3][5], int row, int col) {
    int i, j;
    for (i = 0; i < row; i++) {
        for (j = 0; j < col; j++) {
            // 打印当前元素后跟一个空格
            printf("%d ", arr[i][j]);
        }
        // 每打印完一行后换行
        printf("\n");
    }
}

// print_arr2使用一个数组指针作为参数
void print_arr2(int (*arr)[5], int row, int col) {
    int i, j;
    for (i = 0; i < row; i++) {
        for (j = 0; j < col; j++) {
            // 打印当前元素后跟一个空格
            printf("%d ", arr[i][j]);
        }
        // 每打印完一行后换行
        printf("\n");
    }
}

int main() {
    // 初始化一个3行5列的二维数组
    int arr[3][5] = {{1,2,3,4,5}, {6,7,8,9,10}, {11,12,13,14,15}};
    // 调用print_arr1来打印数组
    print_arr1(arr, 3, 5);
    // 调用print_arr2来打印数组
    print_arr2(arr, 3, 5);
    return 0;
}
对于函数 print_arr1
这个函数通过普通二维数组参数来接收数组数据。尽管形式参数定义了数组的尺寸,实际上在函数内部这个数组类型被解释为指向数组首行的指针。因此,arr[3][5] 可以视作 int (*arr)[5]。
对于函数 print_arr2
这个函数使用了数组指针int (*arr)[5]作为参数。这里的arr是一个指针,指向包含5个整数的数组。这种表示方式更直接地表明arr是指向第一个包含5个整数的数组的指针。
对于主函数
首先定义并初始化一个3行5列的二维数组arr。接着调用两个不同的函数来打印这个数组,尽管函数签名不同(数组类型与数组指针),但由于数组在作为参数传递时退化为指向其首元素(这里是第一行)的指针,两种形式都能正确地处理和打印二维数组。

3.5 综合示例

int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];

第一个是基本整数数组,arr 是一个包含5个整数的数组。

第二个是指针数组,parr1 是一个数组,包含10个指向整数的指针。这意味着每个数组元素都是一个指针,可以指向一个整数。

第三个是数组指针,parr2 是一个指针,指向一个包含10个整数的数组。它不是一个数组,而是一个单独的指针。

第四个是指向数组的指针数组,parr3 是一个数组,包含10个指针,其中每个指针指向一个包含5个整数的数组。

4. 数组参数、指针参数

将一维数组作为参数传递给函数时,实际传递的是数组的第一个元素的地址,而不是整个数组的副本。这是因为数组名在大多数表达式中会退化为指向其首元素的指针。因此,传递数组给函数时,通常是传递指向数组首地址的指针。这种传递方式使得函数能够通过这个指针访问和修改原始数组的内容。

4.1 一维数组传参

1. 通过指针传递

最简单且最常见的一种方式是直接将数组作为指针传递。这种方法不需要在函数原型中指定数组的大小。

#include <stdio.h>

// 定义一个函数,接收一个整数指针和一个表示数组大小的整数
void printArray(int *array, int size) {
    // 循环遍历数组,从0到size-1
    for (int i = 0; i < size; i++) {
        // 打印每个数组元素后跟一个空格
        printf("%d ", array[i]);
    }
    // 所有元素打印完毕后换行
    printf("\n");
}

int main() {
    // 初始化一个整数数组
    int arr[] = {1, 2, 3, 4, 5};
    // 调用printArray函数,传递数组和数组的大小
    printArray(arr, 5);
    return 0;
}

在这个示例中,printArray 函数接收一个指向整数的指针和数组的大小。在 main 函数中,数组 arr 的名称在调用 printArray 时退化为指向其首元素的指针。

2. 通过固定大小数组参数传递

在函数参数中明确数组的大小,尽管这样做并不改变传递方式(仍然是传递指针),但它可以提供额外的语义信息,表明函数预期的数组大小。

3. 通过可变长度数组参数传递(C99特性)
#include <stdio.h>

// 定义一个函数,第一个参数是数组的大小,第二个参数是可变长度的整数数组
void printArray(int size, int array[size]) {
    // 使用第一个参数size来控制循环遍历数组
    for (int i = 0; i < size; i++) {
        // 打印每个数组元素后跟一个空格
        printf("%d ", array[i]);
    }
    // 所有元素打印完毕后换行
    printf("\n");
}

int main() {
    // 初始化一个整数数组
    int arr[] = {1, 2, 3, 4, 5};
    // 调用printArray函数,传递数组的大小和数组本身
    printArray(5, arr);
    return 0;
}

在这个示例中,printArray 的第二个参数是一个可变长度数组,其大小由第一个参数指定。这允许函数在编译时知道数组的确切大小,但实际上这种方法仍然通过地址传递数组。

4.2 二维数组传参

在C中,将二维数组作为参数传递给函数涉及到类似一维数组的指针退化行为,因为同时处理两个维度的数据。

前面提到过,二维数组可以视为数组的数组。在内存中,二维数组通常以行主顺序存储,即数组的行连续存放。例如,定义一个二维数组 int arr[3][4] 表示一个有3行4列的数组。

1. 明确列维度的数组参数

最常见且最直接的方法是在函数参数中明确指定数组的列数。行数可以是变量,但列数必须是常量,这样编译器才能正确计算行跳转。

#include <stdio.h>

// 定义一个函数,接受一个二维数组和一个整数表示行数
void printMatrix(int arr[][4], int rows) {
    // 外层循环遍历每一行
    for (int i = 0; i < rows; i++) {
        // 内层循环遍历每一列
        for (int j = 0; j < 4; j++) {
            // 打印当前元素后跟一个空格
            printf("%d ", arr[i][j]);
        }
        // 完成一行的打印后换行
        printf("\n");
    }
}

int main() {
    // 初始化一个3行4列的二维数组
    int matrix[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
    // 调用函数printMatrix,传递二维数组和行数
    printMatrix(matrix, 3);
    return 0;
}

在这个例子中,printMatrix 函数接受一个每行有4列的整数二维数组。行数通过参数传递,而列数在函数定义中固定。

2. 使用指针到数组的指针

你可以将二维数组作为一个指向含有特定列数的一维数组的指针来传递。这种方式在语法上比较复杂,但它提供了更多的灵活性。

#include <stdio.h>

// 定义一个函数,接受一个指向含有4个整数的数组的指针和一个整数表示行数
void printMatrix(int (*arr)[4], int rows) {
    // 外层循环遍历每一行
    for (int i = 0; i < rows; i++) {
        // 内层循环遍历每一列
        for (int j = 0; j < 4; j++) {
            // 打印当前元素后跟一个空格
            printf("%d ", arr[i][j]);
        }
        // 完成一行的打印后换行
        printf("\n");
    }
}

int main() {
    // 初始化一个3行4列的二维数组
    int matrix[3][4] = {{1, 2, 3, 4}, {5

在这种情况下,printMatrix 接收一个指向有4个整数的数组的指针,它实际上是一个指向二维数组第一行的指针。

传递二维数组时,必须确保至少列的维度在函数参数中是已知的。这是因为二维数组在退化为指针时需要保持足够的信息来计算跨行的数据访问。

4.3 一级指针传参

一级指针,通常简称为指针,是一个变量,其存储的是另一个变量的内存地址。当将一个指针作为参数传递给函数时,实际上是在传递指向某个数据的内存地址。

一级指针传参的作用

  • 数据修改: 通过指针传递,函数可以直接修改原始数据。
  • 内存效率: 传递数据的指针而不是数据本身,可以减少内存使用和提高程序效率。
  • 功能扩展: 指针传参使函数能够处理动态内存分配的数据,以及创建和操作复杂的数据结构,如链表、树、图等。
 1. 修改基本数据类型的值
#include <stdio.h>

// 定义一个函数,使用指针参数来修改传入的整数的值
void increment(int *ptr) {
    (*ptr)++;  // 使用解引用操作符(*)来修改指针指向的值
}

int main() {
    int a = 10;
    increment(&a);  // 传递a的地址
    printf("a after increment: %d\n", a);  // 输出修改后的值
    return 0;
}

在这个示例中,increment 函数通过指针接收一个整数的地址,并在函数内部直接修改这个整数的值。这显示了如何通过指针传参来修改函数外部定义的变量的值。

2. 处理数组
#include <stdio.h>

// 使用指针参数处理数组
void printArray(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);  // 通过指针访问数组元素
    }
    printf("\n");
}

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    printArray(numbers, 5);  // 数组名作为指针传递
    return 0;
}

这个示例展示了如何使用指针来处理数组。在 C 中,数组名在传递给函数时自然退化为指向数组首元素的指针。

04-22 08:16