【C语言回顾】指针(超详细!!一篇文章搞定!!!)

在这里插入图片描述
在这里插入图片描述


上期回顾: 【C语言回顾】操作符详解
个人主页:C_GUIQU
专栏:【C语言学习】

在这里插入图片描述

前言

各位小伙伴大家好!
今日天有晴,阳光灿烂;今日地有情,花团锦簇;今日海有情,浪迭千重;今日人有情,欢聚一堂!
指针是C语言的灵魂,同时也是大多数人的噩梦。不懂指针,相当于没学C语言。
上期小编给大家讲解了C语言中的操作符,接下来我们好好地细说指针!

在这里插入图片描述

1. 深入理解指针(一)

1.1 指针概括

【指针概念】指针是一个变量,它的值是一个内存地址。

当我们说一个指针指向一个变量时,实际上是指它存储了该变量的内存地址。

  • 指针的类型:指针有特定的类型,这决定了它可以指向的数据类型。

例如,int* 是一个指针类型,它可以指向一个整数类型的变量。

  • 指针的声明:指针需要声明其类型。

例如,int* ptr; 声明了一个整数指针变量 ptr。

  • 指针的初始化:指针在使用前需要被初始化,即给它一个有效的内存地址。

这通常通过取变量地址来实现,例如int num = 10; int* ptr = & num;。

  • 指针的解引用:指针的解引用是指通过指针访问它所指向的变量。

例如,*ptr 表示指针 ptr 所指向的变量。

  • 指针的算术运算:指针可以进行算术运算,以移动其在内存中的位置。

例如,ptr += 1 会将 ptr 移动到下一个内存地址。

  • 指针与数组:指针可以用来访问数组中的元素。

例如,int arr[10]; int* ptr = arr; 之后,ptr[3] 将会访问数组 arr 的第四个元素。

  • 指针与函数:指针可以作为函数的参数传递。这允许函数修改调用者提供的内存地址所指向的数据。

  • 指针与动态内存分配:指针可以用来分配和释放动态内存,例如使用 malloc、calloc、realloc 和 free 函数。

1.2 内存和地址

“内存”通常指的是存储器,它用来存储程序和数据。
“地址”是指向内存中特定位置的指针。
【理解】内存单元的编号 = 地址 = 指针

在计算机中我们把内存单元的编号称为地址。C语⾔中给地址起了一个新的名字:指针

在这里插入图片描述

1.3 指针变量和地址

1.3.1 指针变量

  • 指针变量是一种特殊类型的变量,它存储的是内存地址,而不是数据本身。
  • 指针变量的值就是它所指向的数据在内存中的地址。
  • 指针变量的类型决定了它能够指向的数据类型。例如,int* 是一个指针类型,它可以指向一个整数类型的变量。

1.3.2 地址

  • 地址是指向内存中特定位置的指针。
  • 每个变量在内存中都有一个唯一的地址,这个地址是该变量在内存中的位置标识。
  • 通过指针,程序可以访问和修改内存中的数据,因为指针提供了数据在内存中的位置信息。

1.3.3 指针变量和地址的关系

  • 指针变量存储的值是内存地址。
  • 指针变量的类型决定了它能够指向的数据类型。
  • 通过指针变量,程序可以间接地访问它所指向的内存地址中的数据。

1.4 解引用操作符

解引用操作符通常表示为 *
【用法】

  • 声明一个指针变量。例:int* ptr;
  • 初始化指针,使其指向一个变量。例:int num = 10; ptr = & num;
  • 使用解引用操作符获取指针所指向的值。例:int value = *ptr; 此时 value 将被设置为 num 的值。

1.5 指针变量的大小

在大多数现代计算机系统中,指针的大小取决于指针所指向的数据类型。指针本身通常占用的空间与操作系统和编译器有关,但通常情况下,指针的大小是固定的,并且与指针所指向的数据类型无关。
在64位操作系统中,指针的大小通常是8字节(64位),而在32位操作系统中,指针的大小通常是4字节(32位)。这意味着,无论指针指向什么类型的数据,指针本身的大小都是固定的。
例如,假设有一个整数指针 int* ptr;,在64位系统中,ptr的大小是8字节,无论它指向什么整数类型的变量。同样,在32位系统中,ptr 的大小是4字节。
需要注意的是,指针的大小与指针所指向的数据类型的大小是不同的。例如,一个整数指针 int* ptr; 指向一个整数 int num = 10;,在这个例子中,num 的大小取决于它存储的整数的大小,例如在大多数系统上,整数通常是4字节。而指针 ptr的大小,如前所述,取决于操作系统和编译器。

【结论】
• 32位平台下地址是32个bit位,指针变量大小是4个字节。
• 64位平台下地址是64个bit位,指针变量大小是8个字节。
• 注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,大小都是相同的。

1.6 指针变量类型的意义

1.6.1 指针的解引用

【解引用基本语法】

数据类型 *指针变量名;

【解引用的步骤】

  1. 声明一个指针变量,并指定它将指向的数据类型。
  2. 初始化指针变量,使其指向一个有效的内存地址。
  3. 使用解引用操作符 *来访问指针所指向的内存地址中的值。

【解引用的示例】

//假设我们有一个整数变量 int num = 10;,我们想要通过一个指针来访问这个变量的值。
int num = 10; // 声明并初始化整数变量
int *ptr; // 声明一个整数指针变量

ptr = # // 初始化指针变量,使其指向 num 的内存地址

// 解引用指针变量,访问 num 的值
int value = *ptr; // value 将被设置为 num 的值
//在这个例子中,*ptr 表示解引用操作,它访问了指针 ptr 所指向的内存地址中的值,并将这个值赋给了变量 value。

1.6.2 指针的简单加减

  • 指针加整数:当指针加上一个整数时,它指向的内存地址会增加或减少相应数量的内存单元。例如,如果 ptr 是一个指向整数的指针,那么 ptr += 1 会将 ptr 移动到下一个整数的地址。
  • 指针减整数:当指针减去一个整数时,它指向的内存地址会减少或增加相应数量的内存单元。例如,如果 ptr 是一个指向整数的指针,那么 ptr -= 1 会将 ptr 移动到前一个整数的地址。
  • 指针加减指针:指针还可以进行加减运算,这通常用于数组操作。例如,如果 ptr1 和 ptr2 都是指向数组中元素的指针,那么 ptr1 += 2 会将 ptr1 移动到数组中第三个元素的地址,而 ptr1 -= ptr2 会将 ptr1 移动到 ptr2 所指向的元素的下一个元素的地址。
  • 指针加减指针类型:指针还可以进行加减运算,其中一个是整数类型,另一个是指针类型。这通常用于数组操作和动态内存分配。例如,如果 ptr1是一个指向数组的指针,那么 ptr1 += 2 会将 ptr1 移动到数组中第三个元素的地址。

1.6.3 void* 指针

在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为⽆具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性, void* 类型的指针不能直接进行指针的±整数和解引⽤的运算。
【示例】

#include <stdio.h>

int main()
{
	int a = 10;
	int* pa = &a;
	char* pc = &a;
	return 0;
}

【使用void*类型的指针接收地址】

#include <stdio.h>
int main()
{
	int a = 10;
	void* pa = &a;
	void* pc = &a;

	*pa = 10;
	*pc = 0;
	return 0;
}

1.7 const 修饰指针

1.7.1 const修饰变量

const 关键字可以用来修饰变量,以表明该变量是不可变的,即其值在程序运行过程中不能被改变。使用 const
修饰变量可以提高代码的清晰度和可维护性,因为它明确地告诉程序员该变量是一个常量。

以下是关于 const 修饰变量的几个要点:

  1. 定义常量:在C和C++中,可以使用 const 关键字定义常量。例如:
    const int MAX_VALUE = 100; // 定义一个整数常量
    
  2. 局部变量:在函数内部,可以使用 const 来修饰局部变量,以表明该变量在函数执行过程中不会被修改。例如:
    void myFunction() 
    {
        const int x = 10; // 定义一个局部常量
        // x = 20; // 错误,常量不能被重新赋值
    }
    
  3. 全局变量:在函数外部,可以使用 const 来修饰全局变量,以表明该变量在程序运行过程中不会被修改。例如:
    const int GLOBAL_CONSTANT = 100; // 定义一个全局常量
    
  4. 指针和引用:在C和C++中,const 也可以用来修饰指针和引用,以表明指针或引用所指向的数据是不可变的。例如:
    const int *ptr; // 指针指向的数据是常量
    const int &ref = num; // 引用所指向的数据是常量
    

使用 const 修饰变量时,需要确保在声明时已经初始化,因为常量在声明后不能被修改。此外,const 修饰的变量在内存中通常会有特殊处理,以保证其值不会被意外修改。

1.7.2 const修饰指针变量

const 关键字可以用来修饰指针变量,这有助于区分指针指向的数据是否可以被修改。const 修饰指针变量有几种不同的用法:

  1. 常量指针:当 const 修饰指针变量时,它表示指针本身是可变的,即指针可以被重新赋值以指向其他内存地址,但是指针所指向的数据不能被修改。例如:
    const int *ptr; // 指针指向的数据不可修改
    
  2. 指向常量的指针:当 const 放在指针变量后面时,它表示指针指向的数据是可变的,但是指针本身是不可变的,即指针不能被重新赋值以指向其他内存地址。例如:
    int *const ptr; // 指针本身不可变
    
  3. 指向常量的常量指针:当 const 关键字同时放在指针变量前面和后面时,表示指针本身和它所指向的数据都是不可变的。这意味着指针不能被重新赋值,也不能修改它所指向的数据。例如:
    const int *const ptr; // 指针本身和指向的数据都是不可变的
    

使用 const 修饰指针变量时,需要根据实际需要选择合适的修饰方式,以确保指针的行为符合预期,并且能够有效地保护数据不被意外修改。

1.8 指针运算

【分类】
• 指针± 整数
• 指针-指针
• 指针的关系运算

1.8.1 指针 + 整数

#include <stdio.h>

int main()
{
	int arr[10] ={ 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	int sz = sizeof(arr)/sizeof(arr[0]);
	for(i=0; i<sz; i++)
	{
		printf("%d ",*(p+i));//p+i 这⾥就是指针+整数
	}
	return 0;
}

1.8.2 指针 - 指针

#include <stdio.h>

int my_strlen(char *s)
{
 char *p = s;
 while(*p != '\0' )
 p++;
 return p-s;
}
int main()
{
 printf("%d\n", my_strlen("abc"));
 return 0;
}

1.8.3 指针的关系运算

#include <stdio.h>

int main()
{
	int arr[10] ={ 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	int sz = sizeof(arr)/sizeof(arr[0]);
	while(p<arr+sz) //指针的⼤⼩⽐较
	{
		printf("%d ",*p);
		p++;
	}
	return 0;
}

1.9 野指针

【概念】野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

1.9.1 野指针成因

【指针未初始化】

#include <stdio.h>
int main()
{ 
	 int *p;//局部变量指针未初始化,默认为随机值
	 *p = 20;
	 return 0;
}

【指针越界访问】

#include <stdio.h>
int main()
{
	 int arr[10] = {0};
	 int *p = &arr[0];
	 int i = 0;
	 for(i=0; i<=11; i++)
	 {
	 //当指针指向的范围超出数组arr的范围时,p就是野指针
	 *(p++) = i;
	 }
	 return 0;
}

【指针指向的空间释放】

#include <stdio.h>
int* test()
{
	int n = 100;
	return &n;
}

int main()
{
	int* p = test();
	printf("%d\n",*p);
	return 0;
}

1.9.2 如何规避野指针

1.9.2.1 指针初始化
#include <stdio.h>
int main()
{
	 int num = 10;
	 int*p1 = &num;
	 int*p2 = NULL; 
	 return 0;
}
1.9.2.2 小心指针越界

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是
越界访问。

1.9.2.3 指针变量不再使⽤时,及时置NULL,指针使用之前检查有效性
#include<stdio.h>

int main()
{
	int arr[10] ={ 1,2,3,4,5,67,7,8,9,10 };
	int* p = &arr[0];
	for(i=0; i<10; i++)
	{
		*(p++) = i;
	}
	//此时p已经越界了,可以把p置为NULL
	p = NULL;
	//下次使⽤的时候,判断p不为NULL的时候再使⽤
	//...
	p = &arr[0];//重新让p获得地址
	if(p != NULL) //判断
	{
		//...
	}
	return 0;
}

1.10 assert断言

1.10.1 assert断言理解

在C语言中,assert 是一个宏,用于在程序运行时进行断言检查。断言是一种条件表达式,它在程序执行时被评估。如果断言表达式的值为 true,程序继续执行;如果为 false,程序会停止执行并抛出一个错误。
以下是关于 assert 断言的一些要点:

  1. 定义assert 是一个宏,通常在C语言标准库中定义。在C中,它通常位于 <assert.h> 头文件中。
  2. 使用assert 宏的基本语法如下:
    assert(表达式);
    
    这里,表达式 是一个条件表达式。如果 表达式 的值为 true,程序继续执行;如果为 false,程序将调用预定义的错误处理函数,通常会输出错误信息并终止程序。
  3. 默认行为:如果 表达式 的值为 falseassert 宏会调用预定义的错误处理函数,通常包括打印错误信息并终止程序。在C中,默认行为是调用 abort() 函数;在C++中,默认行为是调用 std::abort() 函数。
  4. 自定义行为:在某些情况下,你可能希望自定义 assert 的行为。这可以通过定义一个 assert 函数来实现,该函数在 assert 宏被调用时被调用。例如:
    void my_assert(const char *expr, const char *file, int line) {
        fprintf(stderr, "Assertion failed: %s, file %s, line %d\n", expr, file, line);
        abort(); // 终止程序
    }
    
  5. 调试与发布:在调试模式下,assert 通常会被编译器包含在程序中,并且在断言失败时会抛出错误。在发布模式下,可以通过预处理器指令(如 NDEBUG)来禁用 assert,以提高程序的性能和减少不必要的错误消息。
    总之,assert 是一个非常有用的工具,用于在程序运行时进行断言检查。它可以帮助开发者发现潜在的错误,并确保程序在预期条件下正常工作。它通常在调试模式下使用,以确保程序的稳定性和正确性。在发布模式下,可以通过预处理器指令来禁用 assert,以提高程序的性能和减少不必要的错误消息。

1.10.2 assert断言举例

在C语言中,assert 宏通常用于调试目的,以确保程序在特定条件下运行。以下是一些使用 assert 宏的例子:

  1. 检查输入参数
    #include <stdio.h>
    #include <assert.h>
    int add(int a, int b) {
        assert(a >= 0 && b >= 0); // 确保参数非负
        return a + b;
    }
    int main() {
        int result = add(-1, 2);
        printf("Result: %d\n", result);
        return 0;
    }
    
  2. 检查数组边界
    #include <stdio.h>
    #include <assert.h>
    void printArray(int arr[], int size) {
        assert(size > 0); // 确保数组不为空
        for (int i = 0; i < size; i++) {
            printf("%d ", arr[i]);
        }
        printf("\n");
    }
    int main() {
        int arr[] = {1, 2, 3, 4, 5};
        printArray(arr, 0); // 断言失败,因为数组为空
        return 0;
    }
    
  3. 检查动态内存分配
    #include <stdio.h>
    #include <stdlib.h>
    #include <assert.h>
    int* createArray(int size) {
        int *ptr = malloc(size * sizeof(int));
        assert(ptr != NULL); // 确保动态内存分配成功
        return ptr;
    }
    int main() {
        int *arr = createArray(0); // 断言失败,因为尝试分配零大小
        return 0;
    }
    

在这些例子中,assert 宏用于检查某些条件是否满足,这些条件通常是程序运行的基础假设。如果这些条件不满足,assert 宏将抛出一个错误,并终止程序。这有助于在开发过程中及时发现潜在的问题,并确保程序在实际运行时不会遇到未定义的行为。在发布版本中,可以通过预处理器指令 NDEBUG 来禁用 assert 宏,以提高程序的性能。

1.11 指针的使用和传址调用

1.11.1 strlen的模拟实现

strlen 函数是C语言标准库中的一个函数,用于计算字符串的长度,不包括字符串末尾的 \0 字符。在某些情况下,如果标准库不可用或者需要更深入理解字符串处理,可以模拟实现 strlen 函数。
以下是 strlen 函数的模拟实现:

#include <stdio.h>
int my_strlen(const char *s)
{
    int length = 0;
    while (*s++) {
        length++;
    }
    return length;
}
int main()
{
    char str[] = "Hello, World!";
    printf("The length of the string is: %d\n", my_strlen(str));
    return 0;
}

在这个实现中,我们定义了一个名为 my_strlen 的函数,它接受一个指向字符串的指针 s。函数内部使用一个循环来遍历字符串中的每个字符,并使用 length 变量来计数。循环继续直到遇到字符串末尾的 \0 字符,此时循环结束,length 变量中存储的就是字符串的长度。
需要注意的是,my_strlen 函数是按值传递字符串的,这意味着它不会修改原始字符串。如果需要修改原始字符串,可以使用指针来传递字符串。此外,my_strlen 函数返回的是字符串的长度,不包括 \0 字符。

1.11.2 传值调用和传址调用

在C语言中,函数参数的传递方式主要有两种:传值调用(Call by Value)和传址调用(Call by Reference)。

1.11.2.1 传值调用(Call by Value)
  • 在传值调用中,函数接收的是参数的值,而不是参数的地址。
  • 这意味着在函数内部对参数的任何修改都不会影响原始变量。
  • 传值调用适用于基本数据类型(如整数、浮点数、字符等)和数组。
  • 示例代码:
    void swap(int a, int b) 
    {
        int temp = a;
        a = b;
        b = temp;
    }
    int main() 
    {
        int x = 10, y = 20;
        swap(x, y);
        printf("x = %d, y = %d\n", x, y); // 输出:x = 10, y = 20
        return 0;
    }
    
1.11.2.2 传址调用(Call by Reference)
  • 在传址调用中,函数接收的是参数的地址,而不是参数的值。
  • 这意味着在函数内部对参数的任何修改都会影响原始变量。
  • 传址调用通常通过指针来实现,适用于指针和数组。
  • 示例代码:
    void swap(int *a, int *b) 
    {
        int temp = *a;
        *a = *b;
        *b = temp;
    }
    int main()
    {
        int x = 10, y = 20;
        swap(&x, &y);
        printf("x = %d, y = %d\n", x, y); // 输出:x = 20, y = 10
        return 0;
    }
    

在C语言中,数组名实际上是一个指向数组首元素的指针,因此数组可以通过指针进行传址调用。然而,在函数内部,数组名不能直接作为指针使用,因为数组名是一个常量指针,不能被重新赋值。要解决这个问题,可以使用指针作为参数,或者使用数组指针(指向数组的指针)作为参数。

2. 深入理解指针(二)

2.1 数组名

在C语言中,数组名是一个指针,它指向数组的第一个元素。这意味着数组名可以被看作是一个指向数组第一个元素的指针,这个指针是不可变的,即它不能被重新赋值。
以下是关于数组名的几个要点:

  1. 数组名是一个指针:在C语言中,数组名是一个指针,它指向数组的第一个元素。例如,如果有一个整数数组 int arr[10];,那么 arr 是一个指针,它指向 arr[0]
  2. 数组名作为函数参数:当将数组作为函数参数传递时,通常传递的是数组名,而不是数组的副本。这意味着函数将接收数组的第一个元素的地址。例如:
    void printArray(int arr[]) {
        // arr 是一个指向 int 类型的指针,它指向 arr[0]
    }
    
  3. 数组名作为函数返回值:函数可以返回数组的第一个元素的地址,即数组名。例如:
    int* createArray(int size) {
        int *newArr = malloc(size * sizeof(int));
        return newArr; // 返回新分配数组的第一个元素的地址
    }
    
  4. 数组名作为指针解引用:数组名可以被解引用,即通过数组名获取它所指向的值。例如:
    int value = *arr; // 获取 arr 指向的值,即 arr[0]
    
  5. 数组名作为指针的比较:数组名可以与指针进行比较,以确定它们是否指向同一个数组。例如:
    if (arr == anotherArr) {
        // arr 和 anotherArr 指向同一个数组
    }
    

需要注意的是,虽然数组名是一个指针,但它不能被重新赋值,这意味着你不能通过数组名来改变它所指向的数组。此外,数组名通常用于传递数组的第一个元素的地址,而不是整个数组的副本。

2.2 使用指针访问数组

在C语言中,使用指针访问数组是一种常见的做法,因为数组名本身就是一个指向数组第一个元素的指针。通过指针,可以有效地访问数组的元素,并进行各种操作。
以下是使用指针访问数组的几个例子:

  1. 使用指针访问数组元素
    int arr[10];
    int *ptr = arr;
    int value = *ptr; // 访问 arr[0]
    value = *(ptr + 1); // 访问 arr[1]
    
  2. 使用指针遍历数组
    int arr[10];
    int *ptr = arr;
    for (int i = 0; i < 10; i++) {
        value = *(ptr + i); // 访问 arr[i]
        // 对 value 进行操作
    }
    
  3. 使用指针修改数组元素
    int arr[10];
    int *ptr = arr;
    *ptr = 10; // 修改 arr[0]
    *(ptr + 1) = 20; // 修改 arr[1]
    
  4. 使用指针进行数组复制
    int src[10], dest[10];
    int *src_ptr = src, *dest_ptr = dest;
    for (int i = 0; i < 10; i++) {
        *(dest_ptr + i) = *(src_ptr + i); // 复制 src 数组到 dest 数组
    }
    
  5. 使用指针进行数组排序
    int arr[10];
    int *ptr = arr;
    // 实现排序算法,例如冒泡排序
    

在使用指针访问数组时,需要注意以下几点:

  • 指针的加减运算:指针的加减运算用于移动指针在内存中的位置。例如,ptr += 1 将指针移动到下一个元素的地址,ptr -= 1 将指针移动到前一个元素的地址。
  • 指针的比较:指针可以进行比较运算,以确定它们是否指向同一个数组或指向数组中的相同元素。例如,if (ptr == arr) 用于比较指针和数组名是否指向同一个数组。
  • 数组越界:在使用指针访问数组时,需要注意不要访问数组范围之外的内存,否则可能导致程序崩溃或数据损坏。例如,ptr = arr + 10; 将指针移动到数组范围之外,这是不允许的。
    总之,使用指针访问数组可以提高代码的效率和灵活性,但同时也需要注意安全和避免数组越界。

2.3 一维数组传参的本质

在C语言中,一维数组作为函数参数传递时,其本质是传递数组首元素的地址。这是因为数组名在C语言中本质上是一个指向数组首元素的指针。
具体来说,当一个函数接受一个数组作为参数时,它实际上接收的是数组首元素的地址。这意味着函数内部的任何对数组元素的修改都会影响原始数组,因为它们共享同一个内存位置。
以下是关于一维数组作为函数参数传递的本质的几个要点:

  1. 数组名是首元素的地址:在C语言中,数组名是一个指针,它指向数组的第一个元素。例如,如果有一个整数数组 int arr[10];,那么 arr 是一个指针,它指向 arr[0]
  2. 传递数组名:当将数组作为函数参数传递时,传递的是数组名,即首元素的地址。例如:
    void printArray(int arr[]) {
        // arr 是一个指向 int 类型的指针,它指向 arr[0]
    }
    
  3. 修改数组元素:在函数内部,可以通过指针来修改数组元素。例如:
    void modifyArray(int arr[]) {
        *arr = 10; // 修改 arr[0]
    }
    
  4. 传递数组和指针的区别:虽然数组名可以作为指针使用,但在传递数组名时,实际上传递的是数组首元素的地址。与指针不同,数组名是一个不可变的指针,它不能被重新赋值。
  5. 函数参数传递:当函数的参数是数组名时,函数内部可以通过指针来访问数组中的元素。例如:
    void printArray(int arr[]) {
        for (int i = 0; i < 10; i++) {
            printf("%d ", *(arr + i)); // 打印 arr[i]
        }
        printf("\n");
    }
    

总结来说,一维数组作为函数参数传递的本质是传递数组首元素的地址。这使得函数可以间接地访问和修改数组中的元素,同时保持了原始数组的状态。

2.4 冒泡排序

冒泡排序(Bubble Sort)是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。

以下是冒泡排序的步骤:

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个。
  2. 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后已经排序好的元素。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

【示例】

#include <stdio.h>

void bubbleSort(int arr[], int n) {
    int i, j, temp;
    for (i = 0; i < n-1; i++)      
        // Last i elements are already in place
        for (j = 0; j < n-i-1; j++) 
            // Traverse the array from 0 to n-i-1
            // Swap if the element found is greater
            // than the next element
            if (arr[j] > arr[j+1]) {
                temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
}

// Function to print an array
void printArray(int arr[], int size) {
    int i;
    for (i=0; i < size; i++)
        printf("%d ", arr[i]);
    printf("\n");
}

// Driver code to test above
int main()
{
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int n = sizeof(arr)/sizeof(arr[0]);
    bubbleSort(arr, n);
    printf("Sorted array: \n");
    printArray(arr, n);
    return 0;
}

在这个示例中,bubbleSort 函数接受一个整数数组 arr 和数组的长度 n。它使用两层嵌套循环来实现冒泡排序:外层循环控制需要进行的遍历次数,内层循环负责进行相邻元素的比较和交换。
printArray 函数用于打印排序后的数组。
main 函数是程序的入口点,它定义了一个待排序的数组,调用 bubbleSort 函数进行排序,然后调用 printArray 函数打印排序后的数组。

2.5 二级指针

在C语言中,指针可以用来指向其他指针,这样的指针称为二级指针(Double Pointer)。二级指针通常用于传递指针的指针,或者用于数组的动态内存分配。
以下是关于二级指针的一些要点:

  1. 定义:二级指针是一个指针,它指向一个指针。例如:
    int *ptr; // 指针变量
    int **pptr; // 二级指针变量
    
  2. 指向指针:二级指针指向一个指针,这意味着它存储的是另一个指针的地址。例如:
    int **pptr = &ptr; // pptr 是一个二级指针,它指向 ptr
    
  3. 解引用:二级指针可以通过两次解引用来访问它所指向的指针所指向的数据。例如:
    int **pptr = &ptr;
    int *ptr2 = *pptr; // ptr2 是一个指针,它指向 pptr 所指向的指针所指向的数据
    int value = **pptr; // value 获取 ptr2 所指向的值
    
  4. 动态内存分配:在C语言中,二级指针常用于动态内存分配。例如,使用 malloccalloc 函数为数组分配内存时,通常会使用二级指针来接收分配的内存地址。例如:
    int **pptr = (int **)malloc(sizeof(int *) * size); // 分配 size 个 int 指针的空间
    
  5. 传递指针:在函数参数传递时,可以使用二级指针来传递指针。例如:
    void modifyPointer(int **ptr) {
        *ptr = 10; // 修改指针所指向的值
    }
    int main() {
        int *ptr = malloc(sizeof(int));
        *ptr = 5;
        modifyPointer(&ptr);
        printf("Value: %d\n", *ptr); // 输出:Value: 10
        return 0;
    }
    

在使用二级指针时,需要特别小心,以确保不会访问不存在的内存区域。此外,二级指针通常用于指针的指针和动态内存分配,因此在处理指针时需要谨慎。

2.6 指针数组

在C语言中,指针数组(Pointer Array)是一个数组,其元素都是指针类型。这意味着指针数组中的每个元素都存储了另一个变量的地址。
以下是关于指针数组的几个要点:

  1. 定义:指针数组是一个数组,其元素都是指针类型。例如:
    int *ptr_array[10]; // 定义一个指针数组,可以存储 10 个 int 类型的指针
    
  2. 初始化:指针数组可以通过初始化来为每个元素分配内存地址。例如:
    int *ptr_array[10] = {&a, &b, &c, &d, &e, &f, &g, &h, &i, &j}; // 初始化指针数组,每个元素指向一个整数
    
  3. 访问元素:可以通过索引来访问指针数组中的元素。例如:
    int *ptr = ptr_array[0]; // 获取第一个元素,即指向整数 a 的指针
    
  4. 使用指针数组:指针数组通常用于存储指向多个不同变量的指针,这有助于提高代码的灵活性和可扩展性。例如,在处理多个字符串时,可以使用指针数组来存储每个字符串的地址。
  5. 指针数组与函数:指针数组可以作为函数参数传递,以便函数可以操作多个数据。例如:
    void processStrings(char *strings[], int count) {
        // 函数可以遍历指针数组并处理每个字符串
    }
    int main() {
        char *strings[10] = {"string1", "string2", "string3", "string4", "string5"};
        int count = 5;
        processStrings(strings, count);
        return 0;
    }
    

在使用指针数组时,需要注意以下几点:

  • 指针数组的每个元素必须指向有效的内存地址,否则可能导致程序崩溃或数据损坏。
  • 指针数组的大小是在编译时确定的,因此它的大小不能在运行时改变。如果需要动态分配指针数组的大小,可以使用动态内存分配。
    总之,指针数组是一个数组,其元素都是指针类型,它常用于存储指向多个不同变量的指针。

2.7 指针数组模拟二维数组

在C语言中,指针数组可以用来模拟二维数组。这是因为指针数组中的每个元素都是指向相同数据类型的指针,这些指针指向的数据可以看作是二维数组的行。
以下是使用指针数组模拟二维数组的步骤:

  1. 定义指针数组:首先,定义一个指针数组,其大小与二维数组的行数相同。
  2. 为指针数组分配内存:使用动态内存分配(如 malloccalloc)为指针数组中的每个元素分配内存,每个元素指向一个整数数组(即二维数组的行)。
  3. 初始化指针数组:使用循环为指针数组中的每个元素分配内存,并为每个元素指向的整数数组分配内存。
  4. 使用指针数组:通过指针数组中的元素来访问二维数组的行,并通过行中的指针来访问每个元素。

以下是使用指针数组模拟二维数组的C语言代码示例:

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int rows = 3; // 二维数组的行数
    int cols = 4; // 二维数组的列数
    int **array = (int **)malloc(rows * sizeof(int *));
    // 为指针数组中的每个元素分配内存
    for (int i = 0; i < rows; i++) {
        array[i] = (int *)malloc(cols * sizeof(int));
    }
    // 初始化二维数组
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            array[i][j] = i * cols + j;
        }
    }
    // 打印二维数组
    printf("Initialized 2D array:\n");
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%d ", array[i][j]);
        }
        printf("\n");
    }
    // 释放内存
    for (int i = 0; i < rows; i++) {
        free(array[i]);
    }
    free(array);
    return 0;
}

在这个示例中,我们首先定义了一个指针数组 array,它的大小与二维数组的行数相同。然后,我们使用 malloc 为指针数组中的每个元素分配内存,每个元素指向一个整数数组(即二维数组的行)。接下来,我们使用循环为每个元素指向的整数数组分配内存,并初始化二维数组。最后,我们使用指针数组来访问二维数组的行,并通过行中的指针来访问每个元素。
【注意】在使用指针数组模拟二维数组时,需要特别小心内存管理,确保在不再需要时释放分配的内存。

3. 深入理解指针(三)

3.1 指针变量

3.1.1 字符指针变量

在C语言中,字符指针变量(Character Pointer Variable)是一个指针变量,它存储字符串的地址。字符指针变量通常用于处理字符串和动态内存分配。
以下是关于字符指针变量的几个要点:

  1. 定义:字符指针变量是一个指针,其数据类型是 char*。例如:
    char *str; // 定义一个字符指针变量
    
  2. 初始化:字符指针变量可以通过初始化来赋予一个字符串的地址。例如:
    char *str = "Hello, World!"; // 初始化 str 为 "Hello, World!" 的地址
    
  3. 错误使用:通过字符指针变量来访问和修改字符串。例如:
    char *str = "Hello, World!";
    str[0] = 'H'; // 修改 str 指向的字符串的第一个字符
    str[5] = ' '; // 修改 str 指向的字符串的第六个字符
    

【注意】这段代码实际上修改了原始的字符串常量 “Hello, World!”,这是不推荐的做法,因为字符串常量在程序编译时就已经确定,无法被修改。在实际编程中,我们应该使用动态分配的内存来存储字符串,并在需要修改时使用动态字符串库函数(如 strcpy、strcat 等)来进行操作。
4. 字符串长度:字符指针变量可以用来计算字符串的长度。例如:

char *str = "Hello, World!";
int length = strlen(str); // 获取 str 指向的字符串的长度
  1. 字符串处理函数:字符指针变量可以用来调用字符串处理函数,例如 strcpystrcmpstrcat 等。例如:
    char *str1 = "Hello";
    char *str2 = "World";
    strcpy(str1, str2); // 将 str2 指向的字符串复制到 str1 指向的字符串
    

在使用字符指针变量时,需要注意以下几点:

  • 字符指针变量存储的是字符串的地址,因此它指向的数据是可变的。
  • 字符指针变量可以用来修改字符串中的字符,也可以用来传递字符串给函数。
  • 在使用字符串处理函数时,需要确保字符指针变量指向有效的字符串,否则可能导致程序崩溃或数据损坏。

总之,字符指针变量是一个指针,它存储字符串的地址,并可用于处理字符串和动态内存分配。

3.1.2 数组指针变量

在C语言中,数组指针变量(Array Pointer Variable)是一个指针变量,它存储数组的地址。数组指针变量通常用于处理数组和动态内存分配。
以下是关于数组指针变量的几个要点:

  1. 定义:数组指针变量是一个指针,其数据类型是 数组类型*。例如,如果有一个整数数组 int arr[10];,那么指向这个数组的指针变量可能是 int *ptr;
  2. 初始化:数组指针变量可以通过初始化来赋予一个数组的地址。例如:
    int arr[10];
    int *ptr = arr; // 初始化 ptr 为 arr 的地址
    
  3. 使用:可以通过数组指针变量来访问和修改数组中的元素。例如:
    int arr[10];
    int *ptr = arr;
    ptr[0] = 10; // 修改 arr[0] 的值为 10
    
  4. 数组长度:数组指针变量可以用来计算数组的长度。例如:
    int arr[10];
    int *ptr = arr;
    int length = sizeof(arr) / sizeof(arr[0]); // 获取 arr 的长度
    
  5. 数组处理函数:数组指针变量可以用来调用数组处理函数,例如 memcpymemcmpmemmove 等。例如:
    int arr1[10], arr2[10];
    memcpy(arr1, arr2, sizeof(arr2)); // 将 arr2 的内容复制到 arr1
    

在使用数组指针变量时,需要注意以下几点:

  • 数组指针变量存储的是数组的地址,因此它指向的数据是可变的。
  • 数组指针变量可以用来修改数组中的元素,也可以用来传递数组给函数。
  • 在使用数组处理函数时,需要确保数组指针变量指向有效的数组,否则可能导致程序崩溃或数据损坏。

总之,数组指针变量是一个指针,它存储数组的地址,并可用于处理数组和动态内存分配。

3.1.3 函数指针变量

在C语言中,函数指针变量(Function Pointer Variable)是一个指针变量,它存储函数的地址。函数指针变量通常用于实现函数的动态选择和回调函数。
以下是关于函数指针变量的几个要点:

  1. 定义:函数指针变量是一个指针,其数据类型是 函数类型*。例如,如果有一个返回整数且接收两个整数参数的函数 int add(int a, int b),那么指向这个函数的指针变量可能是 int (*ptr)(int, int);
  2. 初始化:函数指针变量可以通过初始化来赋予一个函数的地址。例如:
    int add(int a, int b) {
        return a + b;
    }
    
    int (*ptr)(int, int) = add; // 初始化 ptr 为 add 函数的地址
    
  3. 使用:可以通过函数指针变量来调用函数。例如:
    int add(int a, int b) {
        return a + b;
    }
    
    int (*ptr)(int, int) = add;
    
    int result = ptr(3, 4); // 调用 add 函数,结果为 7
    
  4. 函数指针作为参数:函数指针可以作为函数的参数传递。例如,可以创建一个函数,它接受一个函数指针作为参数,并调用该函数。
  5. 函数指针数组:可以创建一个函数指针数组,其中每个元素都是一个函数指针。例如:
    int add(int a, int b) {
        return a + b;
    }
    
    int subtract(int a, int b) {
        return a - b;
    }
    
    int (*operations[])(int, int) = {add, subtract}; // 创建一个函数指针数组
    
    int result = operations[0](3, 4); // 调用 add 函数,结果为 7
    

在使用函数指针变量时,需要注意以下几点:

  • 函数指针变量存储的是函数的地址,因此它指向的数据是可变的。
  • 函数指针变量可以用来调用函数,也可以用来传递函数给其他函数。
  • 在使用函数指针时,需要确保它指向一个有效的函数,否则可能导致程序崩溃或数据损坏。

总之,函数指针变量是一个指针,它存储函数的地址,并可用于实现函数的动态选择和回调函数。

3.2 二维数组传参的本质

在C语言中,二维数组作为函数参数传递时,其本质是传递数组首元素的地址。这是因为二维数组在内存中是以连续的内存块形式存储的,数组名实际上指向了该连续内存块的起始地址。
具体来说,当一个函数接受一个二维数组作为参数时,它实际上接收的是数组首元素的地址。这意味着函数内部的任何对数组元素的修改都会影响原始数组,因为它们共享同一个内存位置。
以下是关于二维数组作为函数参数传递的本质的几个要点:

  1. 数组名是首元素的地址:在C语言中,二维数组名是一个指针,它指向数组的第一个元素。例如,如果有一个整数二维数组 int arr[3][4];,那么 arr 是一个指针,它指向 arr[0][0]
  2. 传递数组名:当将二维数组作为函数参数传递时,传递的是数组名,即首元素的地址。例如:
    void printArray(int arr[][4]) {
        // arr 是一个指向 int 类型的指针,它指向 arr[0][0]
    }
    
  3. 修改数组元素:在函数内部,可以通过指针来修改数组元素。例如:
    void modifyArray(int arr[][4]) {
        arr[0][0] = 10; // 修改 arr[0][0]
    }
    
  4. 传递数组和指针的区别:虽然数组名可以作为指针使用,但在传递数组名时,实际上传递的是数组首元素的地址。与指针不同,数组名是一个不可变的指针,它不能被重新赋值。
  5. 函数参数传递:当函数的参数是数组名时,函数内部可以通过指针来访问数组中的元素。例如:
    void printArray(int arr[][4]) {
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 4; j++) {
                printf("%d ", *(*(arr + i) + j)); // 打印 arr[i][j]
            }
            printf("\n");
        }
    }
    

总结来说,二维数组作为函数参数传递的本质是传递数组首元素的地址。这使得函数可以间接地访问和修改数组中的元素,同时保持了原始数组的状态。

3.3 函数指针数组

在C语言中,函数指针数组(Function Pointer Array)是一个数组,其元素都是函数指针。这种数组允许你存储多个函数的地址,并且可以通过索引来选择要调用的函数。
以下是关于函数指针数组的几个要点:

  1. 定义:函数指针数组是一个数组,其元素都是函数指针。例如:
    int (*func_array[])(int, int) = {add, subtract, multiply, divide};
    
  2. 初始化:函数指针数组可以通过初始化来赋予多个函数的地址。例如:
    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) { return a / b; }
    int (*func_array[])(int, int) = {add, subtract, multiply, divide};
    
  3. 使用:可以通过索引来调用函数指针数组中的函数。例如:
    int (*func_array[])(int, int) = {add, subtract, multiply, divide};
    int result = func_array[0](3, 4); // 调用 add 函数,结果为 7
    
  4. 动态选择函数:函数指针数组允许你在运行时动态选择要调用的函数,这在实现回调函数或编写多态性代码时非常有用。
  5. 函数指针作为数组元素:函数指针数组中的每个元素都是一个函数指针,它存储了一个函数的地址。
    在使用函数指针数组时,需要注意以下几点:
  • 函数指针数组中的每个元素必须指向一个有效的函数,否则可能导致程序崩溃或数据损坏。
  • 函数指针数组的大小是在编译时确定的,因此它的大小不能在运行时改变。如果需要动态分配函数指针数组的大小,可以使用动态内存分配。

总之,函数指针数组是一个数组,其元素都是函数指针,它允许你存储多个函数的地址,并通过索引来选择要调用的函数。

3.4 转移表

在C语言中,转移表(Transfer Table)并不是一个标准的数据结构,但它可以根据需要通过不同的数据结构来实现。以下是三个可能的例子,展示了如何在C语言中使用不同的数据结构来模拟转移表的功能:

3.4.1 基于数组的转移表

#include <stdio.h>
#include <stdlib.h>
// 假设我们有一个简单的程序控制流转移表
typedef struct {
    int from;
    int to;
} TransferEntry;
// 初始化一个转移表
TransferEntry transferTable[10] = {
    {0, 1}, {1, 2}, {2, 3},
    // ... 其他转移条目
    {9, 0} // 循环结束
};
// 函数,根据当前状态找到下一个状态
int getNextState(int currentState) {
    for (int i = 0; transferTable[i].from != currentState; i++) {
        if (transferTable[i].from == currentState) {
            return transferTable[i].to;
        }
    }
    return -1; // 没有找到匹配的转移
}
int main() {
    int currentState = 0;
    while (1) {
        currentState = getNextState(currentState);
        if (currentState == -1) {
            break;
        }
        printf("Transfer to state: %d\n", currentState);
    }
    return 0;
}

3.4.2 基于链表的转移表

#include <stdio.h>
#include <stdlib.h>
// 假设我们有一个简单的链表实现的转移表
typedef struct TransferNode {
    int from;
    int to;
    struct TransferNode *next;
} TransferNode;
// 初始化一个转移表
TransferNode *transferTable = NULL;
// 函数,根据当前状态找到下一个状态
int getNextState(int currentState) {
    TransferNode *current = transferTable;
    while (current != NULL) {
        if (current->from == currentState) {
            return current->to;
        }
        current = current->next;
    }
    return -1; // 没有找到匹配的转移
}
// 函数,添加一个新的转移条目到转移表
void addTransfer(int from, int to) {
    TransferNode *newNode = (TransferNode *)malloc(sizeof(TransferNode));
    newNode->from = from;
    newNode->to = to;
    newNode->next = NULL;
    if (transferTable == NULL) {
        transferTable = newNode;
    } else {
        TransferNode *current = transferTable;
        while (current->next != NULL) {
            current = current->next;
        }
        current->next = newNode;
    }
}
int main() {
    addTransfer(0, 1);
    addTransfer(1, 2);
    addTransfer(2, 3);
    // ... 添加更多转移条目
    int currentState = 0;
    while (1) {
        currentState = getNextState(currentState);
        if (currentState == -1) {
            break;
        }
        printf("Transfer to state: %d\n", currentState);
    }
    return 0;
}

3.4.3 计算器的⼀般实现

#include <stdio.h>

int add(int a,int b)
{
	return a + b;
}
int sub(int a,int b)
{
	return a - b;
}
int mul(int a,int b)
{
	return a * b;
}
int div(int a,int b)
{
	return a / b;
}
int main()
{
	int x,y;
	int input = 1;
	int ret = 0;
	do
	{
		printf("*************************\n");
		printf(" 1:add 2:sub \n");
		printf(" 3:mul 4:div \n");
		printf(" 0:exit \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d",&input);
		switch(input)
		{
		case 1:
			printf("输⼊操作数:");
			scanf("%d %d",&x,&y);
			ret = add(x,y);
			printf("ret = %d\n",ret);
			break;
		case 2:
			printf("输⼊操作数:");
			scanf("%d %d",&x,&y);
			ret = sub(x,y);
			printf("ret = %d\n",ret);
			break;
		case 3:
			printf("输⼊操作数:");
			scanf("%d %d",&x,&y);
			ret = mul(x,y);
			printf("ret = %d\n",ret);
			break;
		case 4:
			printf("输⼊操作数:");
			scanf("%d %d",&x,&y);
			ret = div(x,y);
			printf("ret = %d\n",ret);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while(input);
	return 0;
}

4. 深入理解指针(四)

4.1 回调函数

在C语言中,回调函数(Callback Function)是一种可以在程序执行过程中被其他函数调用的函数。回调函数通常作为参数传递给其他函数,并在满足特定条件时由该函数调用。
以下是关于C语言中回调函数的一些要点:

  1. 定义:回调函数是在其他函数中调用的函数。它通常作为参数传递给其他函数,并在该函数执行到特定点时被调用。
  2. 使用场景:回调函数常用于事件驱动编程、图形用户界面(GUI)编程、网络编程等。
  3. 示例:在C语言中,回调函数可以通过指针传递给其他函数。例如,如果有一个函数需要执行某个操作,并且该操作可以通过一个函数来完成,那么可以将这个操作的函数作为回调函数传递给该函数。
  4. 编程语言:回调函数在各种编程语言中都有应用,如C、C++、Java、Python等。
  5. 注意事项:在设计回调函数时,需要确保其函数签名(参数和返回类型)与调用它的函数期望的一致。此外,回调函数通常不应该修改全局状态,以避免潜在的副作用。

以下是C语言中回调函数的一个简单示例:

#include <stdio.h>
// 定义一个回调函数
void printNumber(int number) {
    printf("Number: %d\n", number);
}
// 另一个函数,它接受一个回调函数作为参数
void processNumbers(int numbers[], int size, void (*callback)(int)) {
    for (int i = 0; i < size; i++) {
        callback(numbers[i]);
    }
}
int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    int size = sizeof(numbers) / sizeof(numbers[0]);
    // 传递回调函数
    processNumbers(numbers, size, printNumber);
    return 0;
}

在这个例子中,printNumber 函数是一个回调函数,它被传递给 processNumbers 函数。当 processNumbers 函数遍历数组中的每个数字时,它会调用 printNumber 函数来打印数字。

4.2 qsort函数

4.2.1 qsort函数概括

在C语言中,qsort 函数是一个标准库函数,用于对数组中的元素进行排序。qsort 函数使用快速排序算法(Quick Sort)来对数组进行排序。
以下是关于 qsort 函数的一些要点:

  1. 定义qsort 函数位于 <stdlib.h> 头文件中,用于对数组进行排序。
  2. 使用qsort 函数的基本语法如下:
    void qsort(void *base, size_t num, size_t size, int (*compar)(const void *, const void *));
    
    这里,base 是待排序数组的起始地址;num 是数组中的元素数量;size 是单个元素的大小(以字节为单位);compar 是比较函数,用于比较两个元素的大小。
  3. 比较函数compar 函数必须符合一定的签名,它接受两个指向待比较元素的指针,并返回一个整数。如果第一个元素小于第二个元素,返回负数;如果两个元素相等,返回零;如果第一个元素大于第二个元素,返回正数。例如:
    int compare(const void *a, const void *b) {
        int x = *(int*)a;
        int y = *(int*)b;
        return x - y;
    }
    
  4. 排序类型qsort 函数可以对数组进行升序或降序排序,这取决于比较函数的返回值。
  5. 示例:以下是一个使用 qsort 函数对整数数组进行排序的示例:
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    int compare(const void *a, const void *b) {
        return (*(int*)a - *(int*)b);
    }
    int main() {
        int arr[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
        int n = sizeof(arr) / sizeof(arr[0]);
        // 对数组进行排序
        qsort(arr, n, sizeof(arr[0]), compare);
        // 打印排序后的数组
        for (int i = 0; i < n; i++) {
            printf("%d ", arr[i]);
        }
        printf("\n");
        return 0;
    }
    
  6. 注意事项:在使用 qsort 函数时,需要注意数组元素的类型和大小,以及比较函数的返回值。此外,qsort 函数是递归的,因此在排序大量数据时可能会消耗较多的栈空间。
    总之,qsort 函数是一个非常有用的工具,用于对数组中的元素进行快速排序。它常用于需要对数据进行排序的场景,如文件排序、数据处理等。

4.2.2 使用qsort函数排序整型数据

在C语言中,qsort 函数是一个标准库函数,用于对数组中的元素进行排序。qsort 函数使用快速排序算法(Quick Sort)来对数组进行排序。
以下是使用 qsort 函数对整型数据进行排序的示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 比较函数,用于比较两个整数的大小
int compare(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}
int main() {
    // 创建一个整型数组
    int arr[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
    int n = sizeof(arr) / sizeof(arr[0]); // 数组中元素的个数
    // 使用 qsort 函数对数组进行排序
    qsort(arr, n, sizeof(arr[0]), compare);
    // 打印排序后的数组
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

在这个示例中,我们首先定义了一个整型数组 arr,并计算出数组中元素的个数 n。然后,我们使用 qsort 函数对数组进行排序,并传递一个比较函数 compare 作为参数。这个比较函数比较两个整数的大小,并根据比较结果返回一个整数。最后,我们打印出排序后的数组。
当运行这段代码时,它会输出排序后的整型数组:

1 1 2 3 3 4 5 5 5 6 9 

在这个例子中,我们使用了升序排序。如果需要进行降序排序,可以修改比较函数中的比较逻辑。

4.2.3 使用qsort函数排序结构数据

在C语言中,qsort 函数也可以用来对包含结构体成员的数组进行排序。结构体数组的每个元素都是一个结构体,因此我们需要为每个结构体成员提供一个比较函数。
以下是使用 qsort 函数对结构体数据进行排序的示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义一个结构体
typedef struct {
    int id;
    char name[50];
} Person;
// 比较函数,用于比较两个 Person 结构体对象的 id 成员
int compareById(const void *a, const void *b) {
    Person *personA = (Person *)a;
    Person *personB = (Person *)b;
    return personA->id - personB->id;
}
// 比较函数,用于比较两个 Person 结构体对象的 name 成员
int compareByName(const void *a, const void *b) {
    Person *personA = (Person *)a;
    Person *personB = (Person *)b;
    return strcmp(personA->name, personB->name);
}
int main() {
    // 创建一个 Person 结构体数组
    Person people[] = {
        {1, "Alice"},
        {2, "Bob"},
        {3, "Charlie"},
        {4, "David"},
        {5, "Eve"}
    };
    int n = sizeof(people) / sizeof(people[0]); // 数组中元素的个数
    // 使用 qsort 函数对数组进行排序
    qsort(people, n, sizeof(people[0]), compareById); // 按照 id 排序
    printf("Sorted by ID:\n");
    for (int i = 0; i < n; i++) {
        printf("ID: %d, Name: %s\n", people[i].id, people[i].name);
    }
    printf("\n");
    qsort(people, n, sizeof(people[0]), compareByName); // 按照 name 排序
    printf("Sorted by Name:\n");
    for (int i = 0; i < n; i++) {
        printf("ID: %d, Name: %s\n", people[i].id, people[i].name);
    }
    printf("\n");
    return 0;
}

在这个示例中,我们定义了一个 Person 结构体,它包含一个整数 id 和一个字符串 name。我们为 qsort 函数提供了两个比较函数:compareByIdcompareByNamecompareById 函数用于根据 id 成员对 Person 结构体数组进行排序,而 compareByName 函数用于根据 name 成员进行排序。
当运行这段代码时,它会输出按照 idname 排序后的 Person 结构体数组。

4.3 qsort函数的模拟实现

快速排序(Quick Sort)是一种高效的排序算法,其基本思想是选择一个基准值,将数组分为两部分,一部分小于基准值,一部分大于基准值,然后递归地对这两部分进行排序。qsort 函数是C语言标准库中实现快速排序的函数。
以下是一个简单的qsort函数的模拟实现,用于对整数数组进行排序:

#include <stdio.h>
#include <stdlib.h>
// 比较函数,用于比较两个整数的大小
int compare(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}
// 快速排序函数
void quickSort(void *base, size_t num, size_t size, int (*compar)(const void *, const void *)) {
    if (num <= 1) {
        return;
    }
    int pivotIndex = num / 2;
    int pivot = *(int *)((char *)base + pivotIndex * size);
    int *low = (int *)((char *)base);
    int *high = (int *)((char *)base + (num - 1) * size);
    while (low <= high) {
        while (*low < pivot) {
            low = (int *)((char *)low + size);
        }
        while (*high > pivot) {
            high = (int *)((char *)high - size);
        }
        if (low <= high) {
            int temp = *low;
            *low = *high;
            *high = temp;
            low = (int *)((char *)low + size);
            high = (int *)((char *)high - size);
        }
    }
    quickSort((void *)((char *)base), (size_t)(low - (int *)base), (size_t)(size), compar);
    quickSort((void *)((char *)base + ((low - (int *)base) + 1) * size), (size_t)(num - (high - (int *)base) - 1), (size_t)(size), compar);
}
int main() {
    int arr[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
    int n = sizeof(arr) / sizeof(arr[0]);
    // 对数组进行排序
    quickSort(arr, n, sizeof(arr[0]), compare);
    // 打印排序后的数组
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

在这个模拟实现中,我们首先检查数组的大小,如果小于等于1,则直接返回,因为已经是最小单位,不需要排序。然后,我们选择一个基准值(这里我们选择数组中间的元素作为基准),并将其与数组中所有元素进行比较。在每次比较后,我们将基准值的位置调整到正确的位置,然后递归地对数组的两部分进行排序。
请注意,这个实现是针对整数数组的,并且假设比较函数可以正确地比较整数。如果你需要对其他类型的数据进行排序,你需要修改比较函数和数组元素的类型。

4.4 sizeof 和 strlen

4.4.1 两者概括

在C语言中,sizeofstrlen 是两个用于操作数组和字符串的函数。

  1. sizeof 运算符
    • sizeof 是一个运算符,而不是函数,用于返回数据类型或变量的大小,以字节为单位。
    • 语法:sizeof(类型名)sizeof(变量名)
    • 示例:sizeof(int) 返回整数类型的大小,sizeof(arr) 返回数组 arr 的大小(以字节为单位)。
  2. strlen 函数
    • strlen 是C语言标准库中的一个函数,用于计算字符串的长度,不包括字符串末尾的 \0 字符。
    • 语法:strlen(字符串)
    • 示例:strlen("Hello, World!") 返回字符串 “Hello, World!” 的长度,不包括字符串末尾的 \0

以下是一些示例代码:

#include <stdio.h>
#include <string.h>
int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);
    printf("Array size: %d bytes\n", size);
    char str[] = "Hello, World!";
    int length = strlen(str);
    printf("String length: %d characters\n", length);
    return 0;
}

在这个示例中,我们首先计算整数数组 arr 的大小,然后计算字符串 str 的长度。sizeof 运算符用于计算数组的大小,而 strlen 函数用于计算字符串的长度。

4.4.2 两者区别

sizeofstrlen 在C语言中是两个不同的概念,它们用于不同的目的:

  1. sizeof 运算符
    • sizeof 是一个运算符,而不是函数,用于计算数据类型或变量的大小。
    • 它可以用来计算数组的大小,但需要注意,sizeof 返回的是数组所占用的字节数,而不是数组中元素的数量。
    • 它不能直接用于计算字符串的长度,因为字符串是以 \0 结尾的,sizeof 会计算 \0 的字节数。
  2. strlen 函数
    • strlen 是C语言标准库中的一个函数,用于计算字符串的长度,不包括字符串末尾的 \0 字符。
    • 它专门用于计算字符串的长度,可以直接用于字符串,并且不会计算 \0 字符。
    • 它不能用于计算数组的大小,因为数组和字符串是不同的数据类型。

以下是一些示例代码,展示了它们之间的区别:

#include <stdio.h>
#include <string.h>
int main() {
    int arr[] = {1, 2, 3, 4, 5};
    printf("Array size: %zu bytes\n", sizeof(arr)); // 输出数组所占用的字节数
    char str[] = "Hello, World!";
    printf("String length: %zu characters\n", strlen(str)); // 输出字符串的长度,不包括 '\0'
    printf("Size of 'int': %zu bytes\n", sizeof(int)); // 输出 int 类型的大小
    printf("Size of 'char': %zu bytes\n", sizeof(char)); // 输出 char 类型的大小
    return 0;
}

在这个示例中,我们首先使用 sizeof 运算符计算数组 arr 的大小,然后使用 strlen 函数计算字符串 str 的长度。我们还展示了如何使用 sizeof 运算符来计算基本数据类型的大小。

5. 深入理解指针(五)

5.1 十道经典指针编程题

1.通过地址运算符&获得地址值
2.输入a,b,按从小到大的顺序输出
3.用指针法访问数组元素
4.从键盘输入10个整数,放入一堆数组a中,然后将该数组中的元素值依次输出
5.将10个数的最小值换到最前面的位置
6.求二维数组元素的最大值
7.用指针法实现字符串的复制
8.将具有10个元素的整数型数组中的元素值按逆序存放后输出
9.用一个函数求10个学生成绩的最高分,最低分和平均成绩
10.求10个数中的最大值,通过函数返回最大值元素的地址的方法来实现

5.2 参考答案

1.通过地址运算符&获得地址值

#include<stdio.h>
main()
{
	int a,*p1;
	p1=&a;
	*p1=123;
	printf("%d,%d\n",a,*p1);
	scanf("%d",p1);
	printf("%d,%d\n",a,*p1);
}

2.输入a,b,按从小到大的顺序输出

#include<stdio.h>
main()
{
	int a,b,*p=&a,*q=&b,*t;
	scanf("%d,%d",p,q);
	if(*p<*q){
		t=p;p=q;q=t;
	}
		printf("a=%d,b=%d\n",a,b);
		printf("最大值=%d,最小值=%d\n",*p,*q);
	
}

3.用指针法访问数组元素

#include<stdio.h>
main()
{
	int a[10],i,*p=a;
	for(i=0;i<10;i++)
	scanf("%d",p+i);
	for(i=0;i<10;i++)
	printf("%4d",*(p+i));
	printf("\n");
}

4.从键盘输入10个整数,放入一堆数组a中,然后将该数组中的元素值依次输出

#include<stdio.h>
main()
{
	int *p,i,a[10];
	p=&a[0];
	for(i=0;i<10;i++)
	scanf("%d",p++);
	p=&a[0];
	for(i=0;i<10;i++)
	printf("%4d",*p++);
	printf("\n");
}

5.将10个数的最小值换到最前面的位置

#include<stdio.h>
main()
{
	int t,a[10],*p,*q;
	for(p=a;p<=a+9;p++)
	scanf("%d",p);
	for(q=a,p=a+1;p<=a+9;p++)
	 if(*p<*q)q=p;
	 printf("最小值:%d\n",*q);
	 printf("最小值的位置:%d\n",q-a);
	 t=*a;*a=*q;*q=t;
	 printf("交换之后的10个数是:\n");
	 for(p=a;p<a+10;p++)
	 printf("%4d",*p);
	 printf("\n");
}

6.求二维数组元素的最大值

#include<stdio.h>
main()
{
	int a[3][4]={{5,1,-8,11},{26,-7,10,129},{2,18,7,16}},*p,max;
	for(p=&a[0][0],max=*p;p<&a[0][0]+12;p++)
	if(*p>max)max=*p;
	printf("MAX=%d\n",max);
}

7.用指针法实现字符串的复制

#include<stdio.h>
main()
{
	char a[80],b[80];
	char *p1,*p2;
	gets(a);
	for(p1=a,p2=b;*p1!='\0';p1++,p2++)
	*p2=*p1;
	*p2='\0';
	printf("字符串a中的内容:%s\n",a);
	printf("字符串b中的内容:%s\n",b);
}

8.将具有10个元素的整数型数组中的元素值按逆序存放后输出

#include<stdio.h>
void swap(int *x,int *y)
{
	int t;
	t=*x;
	*x=*y;
	*y=t;
}
main()
{
	int a[10],i;
	for(i=0;i<10;i++)
	scanf("%d",&a[i]);
	for(i=0;i<=4;i++)
	swap(&a[i],&a[10-i-1]);
	for(i=0;i<10;i++)
	printf("%4d",a[i]);
	printf("\n");
}

9.用一个函数求10个学生成绩的最高分,最低分和平均成绩

#include<stdio.h>
float fun(int *x,int n,int *p1,int *p2)
{
	int i;
	float s=0;
	*p1=*p2=x[0];
	for(i=0;i<n;i++)
	{
		s=s+x[i];
		if(*p1<x[i]) *p1=x[i];
		else if(*p2>x[i])*p2=x[i];
	}
	return s/n;
}
main()
{
	int i,a[10],max,min;
	float ave;
	for(i=0;i<10;i++)
	scanf("%d",&a[i]);
	for(i=0;i<10;i++)
	printf("%4d",a[i]);
	ave=fun(a,10,&max,&min);
	printf("\n 平均值=%6.2f,最大值=%d,最小值=%d\n",ave,max,min);
}

10.求10个数中的最大值,通过函数返回最大值元素的地址的方法来实现

#include<stdio.h>
int *fun(int *x,int n)
{
	int i,*y;
	y=x;
	for(i=1;i<n;i++)
	if(*(x+i)>*y)y=x+i;
	return y;
}
main()
{
	int a[10],*p,i;
	for(i=0;i<10;i++)
	scanf("%d",&a[i]);
	p=fun(a,10);
	printf("最大值=%d\n",*p);
}

结语

以上就是小编对指针的详细讲解。
如果觉得小编讲的还可以,还请一键三连。互三必回!
持续更新中~!

我一直在遇见许多不同的、可爱的、善良的人,继而又不舍的与他们分别。能脚踏实地,也仰望星空;既有随处可栖的江湖,也有追风逐梦的骁勇。善良,优秀,勇敢,祝我们所有人。

在这里插入图片描述

在这里插入图片描述

  • 176
    点赞
  • 136
    收藏
    觉得还不错? 一键收藏
  • 182
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 182
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值