目录
- 定义
type *var_name;
- 初始化
int *p = &a; // p指向变量a的地址
- 空指针NULL,野指针,指针悬挂
- 解引用
- 指针的算术运算
- 指针与数组
- 数组名—首指针
- 二维数组指针
- 行指针
- 列指针
- 多级指针
(进阶)
- 数组指针,指针数组
(进阶)
- 指针实现动态数组
(进阶)
- 指针与函数
- 传递指针给函数
- 传递数组给函数
- 冒泡排序
- 函数返回值为指针
(进阶)
- 函数指针和回调函数
(进阶)
- 函数签名
(进阶)
- 函数签名
- 指针与常量
- 常量指针和指针常量
- 指针和结构体
- 结构体指针
typedef
简化结构体指针定义- 结构体指针和函数
- 结构体内嵌指针
(进阶)
- 结构体内嵌结构体指针
(进阶)
- 内嵌其他结构体
(进阶)
- 内嵌自身
(进阶)
- 内嵌其他结构体
- 结构体内嵌函数指针
(进阶)
- 指针与字符和字符串
- 字符指针
- 字符数组(字符串)
- 字符指针数组
- 字符指针与字符串函数
- 动态分配字符串
(进阶)
- 指针与文件I/O
(进阶)
---------------------------------((进阶)
)--------------------------------------------- - 指针与动态内存分配
(进阶)
- 程序四大区
(进阶)
void*
的特性(进阶)
malloc
和free``(进阶)
- 程序四大区
- C语言编译冷知识
(进阶)
- 预处理
(进阶)
- 编译
(进阶)
- 汇编
(进阶)
- 链接。
(进阶)
- 编译器
(进阶)
- 预处理
- Cmake和CmakeList和Makefile
(进阶)
-------------------------------学无止境,越学越远-------------------------------------
1 引入
1-1 内存地址基础概念
- 计算机内存地址是计算机内存中的每一个字节都有一个唯一的编号。在程序中,每个变量都存储在内存的某个地址上。计算机通过内存地址来访问和管理内存中的数据。
- 内存地址通常以十六进制表示,例如
0x1000
。
1-1-1 说人话环节
- 在计算机中,内存地址就像是一个大型仓库中的货架编号,每个货架都有一个唯一的编号。而指针就像是一张卡片,这张卡片上写着某个货架的编号,这样我们就可以通过这张卡片找到存放物品的具体位置。通过使用指针,我们可以轻松地在内存中导航,访问和修改数据。
- 指针(卡片)->内存地址(货架编号),内存地址(货架编号)对应的数据(货物)
1-2 指针的定义
- 指针是一个变量,它存储了另一个变量的内存地址。
- 在C语言中,使用
*
符号来定义指针。
typename* pointer;
- 下面代码我创建了一个类型为
int*
的指针,并给他取名为p。这时候我们创建了一个新的卡片。用货架举例子就是我创建了一个新的卡片。
int * p;
1-3 指针的初始化
- 这时候,卡片还没有设置对应的货架编号,通过这个空的卡片(空指针),还无法找到货架编号(内存地址),更无法找到对应的货物(数据)
- 可以通过
&
运算符获取变量的地址来初始化指针。
int a = 10;
int *p = &a;
- 变量名
a
是一个标识符,它代表了内存中的某个地址。当你给a
初始化时,例如a = 10;
,你实际上是在将值10
存储在a
所代表的内存地址中。- 如果你不初始化a的时候,a对应的数据就是不确定的
&
运算符:取地址运算符:当你对一个变量使用&
运算符时,它会返回该变量的内存地址。例如,如果你有一个整型变量int a = 10;
,那么&a
将返回变量a
的内存地址。- 回顾一下我们之前玩过无数次的
scanf
int a; scanf("%d", &a); // 使用&运算符来传递变量a的地址
- 回顾一下我们之前玩过无数次的
说人话
:用货架的例子就是,我现在有一个货物a,它的货架编号是&a,我创建了一张新的卡片p在上面写上a的货架编号,这样我就可以通过卡片访问到a了- 同理我们创建多张卡片指向同一个内存地址
int b = 10; // 创建另一个货物b,并给它一个初始值10
int *p1; // 创建一张新的卡片p1
int *p2; // 创建另一张新的卡片p2
p1 = &b; // 在卡片p1上写下货物b的货架编号
p2 = &b; // 在卡片p2上也写下货物b的货架编号
1-3-1 NULL指针
- 正如我们创建了一个新的卡片,如果你没有在卡片上写上新的地址信息以初始化,这个指针就是个空指针
- C语言提供了
NULL
指针的概念。NULL
是一个宏,它在标准库中定义,通常在<stddef.h>
头文件中声明。NULL
的值通常被定义为0
,或者在某些平台上是一个特殊的地址,这个地址被设计为不可能是一个有效的内存地址。
int *ptr = NULL; // 将ptr初始化为NULL
if (ptr != NULL) {
// 使用ptr
} else {
// ptr是NULL,不能使用
}
-
如果你尝试访问一个空指针,程序可能会崩溃,因为它试图访问一个无效的内存地址。这是因为内存中的每个地址都有可能被操作系统或程序使用,访问一个未分配或未初始化的地址可能会导致程序出错或系统不稳定。
-
说人话:有个只有酒店门牌号只有300-400,而你却收到了一张404的房卡
1-4 指针的解引用
- 解引用操作使用
*
符号,用于获取指针指向的变量的值。
int a = 10;
int *p = &a;
int b = *p;
- 解引用操作符
*
用于获取指针所指向的变量的值。 - 说人话就是拿着卡片p,找到对应的货物a,拿出a的值(货物),给b
- 需要特别注意区分:
int* a
的int*
是一个类型。int b = *p;
的*
是取地址符号
- 我们来看一个移情别恋(不是)的例子:
int a=10;
int* p=&a;
int b=20;
int* p=&b;
printf("p的值是:%d",*p);
- 弹幕里回复一下p的值
- 答案是20
2 指针与数组
- 上述说了这么多,是不是感觉很迷惑,为什么要引入指针?
2-1 数组首指针
- 在C语言中,数组名本身就是一个指向数组第一个元素的指针。这意味着你可以使用数组名作为指针,并在需要时对数组进行操作,例如排序。
- 下述例子,
array
就是数组第一个元素的地址。下述操作将输出数组的第一个元素
int array[]={1,2,3};
printf("%d",*array);
2-1-1 arr
和 arr[0]
arr
存储的是数组的起始地址,而arr[0]
存储的是第一个元素的值。
int array[] = { 1,2,3 };
//%zu 是 size_t (unsigned int)的标准格式化字符串
printf("sizeof(array):%zu\n", sizeof(array));
printf(" sizeof(array[0]):%zu\n", sizeof(array[0]));
sizeof(arr)
返回整个数组的大小(字节数)。这包括数组中所有元素的总大小。
sizeof(arr[0])
返回数组中单个元素的大小(字节数)。对于int
类型的数组,这通常是 4 字节(在大多数现代架构上)。
2-1-2 指针的算术运算
- 指针可以进行算术运算,如增减。
- 指针的自增(
++
)和自减(--
)运算符会改变指针的值,使其指向数组的下一个或上一个元素。
int array[] = {1, 2, 3};
int *ptr = array; // 指针指向数组的第一个元素
printf("%d\n", *ptr); // 输出第一个元素
ptr++; // 指针指向下一个元素
printf("%d\n", *ptr); // 输出第二个元素
ptr++; // 指针指向下一个元素
printf("%d\n", *ptr); // 输出第三个元素
- 当你对指针进行加法或减法运算时,编译器会根据指针指向的数据类型自动调整偏移量。例如,如果
p
是一个指向int
类型的指针,那么p + 1
实际上是将p
的地址值增加了sizeof(int)
字节。
2-1-3 两种形式
- 前缀形式 (
++ptr
或--ptr
):- 当你使用前缀形式时,指针的值首先增加或减少,然后表达式返回新的指针值。
- 例如,
++ptr
将指针ptr
向前移动一个int
类型的大小(假设ptr
是一个指向int
类型的指针),然后返回移动后的指针。
- 后缀形式 (
ptr++
或ptr--
):- 当你使用后缀形式时,表达式首先返回指针的当前值,然后指针的值增加或减少。
- 例如,
ptr++
首先返回ptr
的当前值,然后ptr
向前移动一个int
类型的大小。
int array[] = {1, 2, 3};
int *ptr = array;
// 前缀形式
printf("Prefix: %d\n", *++ptr); // 输出2,因为ptr先自增,再解引用
// 后缀形式
printf("Postfix: %d\n", *ptr++); // 输出2,因为ptr先解引用,再自增
// 现在ptr指向了数组的第三个元素
printf("Current value: %d\n", *ptr); // 输出3
2-1-4 使用指针遍历数组
- 我们可以使用++和–遍历数组
int arr[] = {1, 2, 3, 4, 5};
int n = sizeof(arr) / sizeof(arr[0]);
int *ptr = arr;
for (int i = 0; i < n; i++)
{
printf("%d ", *ptr);
ptr++;
}
2-2 二维数组指针—行指针和列指针
- 在C语言中,二维数组可以通过行指针和列指针来访问。
行指针
是指向二维数组中一行的指针列指针
是指向二维数组中一列的指针
2-2-1 行指针:多级指针
- 行指针是一个指向包含多个元素的一维数组的指针。
- 当我们声明一个二维数组时,例如
int arr[3][3];
,arr
本身就是一个行指针,它指向二维数组的第一行。第一级指针通常指向数组的行,而第二级指针则指向每行的第一个元素。
int arr[3][3]={
{1,2,3},
{4,5,6},
{7,8,9},
}
int (*row_ptr)[3] = arr;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++) {
printf("%d ", row_ptr[i][j]);
//printf("%d ", *(*(row_ptr + i) + j));
}
printf("\n");
}
- 通过使用
row_ptr
,我们可以访问二维数组中的任意元素。例如,*(*(row_ptr + i) + j)
就可以访问arr
数组的第i
行第j
列的元素。
2-2-2 列指针
- 列指针是一个指向二维数组中单个元素的指针。
- 在C语言中,你可以通过使用运算符
&
来获取一个元素的地址,从而创建一个列指针。例如,如果你想要创建一个指向arr[1][2]
的列指针,你可以这样做:
int *col_ptr = &arr[1][2];
- 我们可以这样进行遍历,
col_ptr
就是一个列指针,它指向arr
数组的第1列的第1行元素。你可以通过递增col_ptr
来访问该列的其他元素。例如,col_ptr + 1
将指向arr
数组的第2列的第1行元素,以此类推。
int arr[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
// 列指针
int *col_ptr = &arr[0][1];
// 通过列指针访问元素
for (int i = 0; i < 3; i++)
{
printf("%d ", *(col_ptr + i));
}
printf("\n");
2-3 多级指针(进阶)
多级指针
是C语言中的一个概念,它指的是指针的指针,即一个指针变量存储了另一个指针变量的地址。- 在C语言中,你可以创建任意深度的多级指针,这在处理复杂的数据结构时非常有用,例如动态二维数组、链表等。
int var = 5; // 声明一个整型变量
int *ptr1; // 声明一个指向整型的指针
int **ptr2; // 声明一个指向整型指针的指针,即多级指针
// 初始化变量和指针
var = 5;
ptr1 = &var; // ptr1 指向 var
ptr2 = &ptr1; // ptr2 指向 ptr1
// 通过多级指针访问变量
printf("Value of var through ptr2: %d\n", **ptr2);
- 还是说人话:卡牌p上记录了pp卡牌的位置,卡牌pp上记录了货物a的位置。通过卡牌p可以找开票pp的位置,通过pp可以找到a的位置。哎,就是套娃
2-3-1 多级指针的用途
- 动态内存分配(后面我们讲到
malloc
再说) - 数据结构,结构体和内嵌指针(后面我们讲到结构体再说)
// 定义二叉树节点结构体
typedef struct TreeNode {
int data;
struct TreeNode* left;
struct TreeNode* right;
} TreeNode;
- 函数返回多个值(后面我们讲到
malloc
,函数和结构体再说)
typedef struct {
int value1;
int value2;
} ReturnValue;
ReturnValue* getTwoValues() {
ReturnValue* result = (ReturnValue*)malloc(sizeof(ReturnValue));
if (result == NULL) {
exit(1);
}
result->value1 = 10;
result->value2 = 20;
return result;
}
ReturnValue* values = getTwoValues(); // 使用多级指针返回多个值
printf("Value 1: %d\n", values->value1);
printf("Value 2: %d\n", values->value2);
2-4 指针数组和数组指针(进阶)
- 在 C 语言中,指针数组和数组指针是两个不同的概念。
2-4-1 指针数组:是数组
- *定义:指针数组是一个数组,其元素是指针。
说人话
:一系列的卡牌,每张卡牌都指向一个不同的货架- *语法:
类型名 *数组名[数组大小];
int *ptrArray[10]; // 这是一个指针数组,包含 10 个指向 int 的指针
ptrArray
是一个数组,它有 10 个元素,每个元素都是int*
类型的指针。- 你可以为这个数组的每个元素分配内存,并使它们指向不同的
int
变量。 - 这种类型的数组常用于存储指向不同数据的指针,例如,可以存储多个字符串的地址。
int a = 10, b = 20, c = 30;
int* p1=&a;
int* p2 = &b;
int* p3 = &c;
int* ptrArray[3];
// 初始化指针数组
ptrArray[0] = p1;
ptrArray[1] = p2;
ptrArray[2] = p3;
int n = sizeof(ptrArray) / sizeof(int*);
for (int i = 0; i < n; i++) {
printf("ptrArray[%d] = %p\n", i, ptrArray[i]);
printf("ptrArray[%d] = %d\n", i, *ptrArray[i]);
}
2-4-2 数组指针:是指针
- *定义:数组指针是一个指针,它指向一个数组。
- 说人话:这张特殊的卡牌,通过它我们可以访问一个特殊的货架,这个货架里放着一系列的卡牌。
- *语法:
类型名(*)[数组大小];
int (*arrayPtr)[10]; // 这是一个指向包含 10 个 int 的数组的指针
arrayPtr
是一个指针,它指向一个包含 10 个int
的数组。- 你可以为这个指针分配内存,并使它指向一个实际的数组。
- 这种类型的指针常用于函数参数,当你需要传递一个数组的地址时,但又不希望改变数组的大小。
int array[]={1,2,3};
// 声明一个数组指针,指向上面声明的数组
int (*arrayPtr)[3] = &array;
int n= sizeof(array) / sizeof(array[0]);
// 输出数组指针指向的数组的内容
for (int i = 0; i < n; i++) {
printf("arrayPtr[%d] = %d\n", i, (*arrayPtr)[i]);
}
2-4-3 数组指针和多级指针
- 数组指针是指向数组的指针。当你声明一个数组指针时,你实际上是在声明一个指针,这个指针指向整个数组
int arr[10]; // 声明一个包含10个整数的数组
int (*arrayPtr)[10]; // 声明一个数组指针,指向包含10个整数的数组
arrayPtr = &arr; // 将数组arr的地址赋给数组指针
- 多级指针是指向指针的指针。当你声明一个多级指针时,你实际上是在声明一个指针,这个指针指向另一个指针。\
int *ptr; // 声明一个指向整数的指针
int **ptrPtr; // 声明一个指向指针的指针,即多级指针
ptrPtr = &ptr; // 将ptr的地址赋给多级指针
2-4-3 用途
指针数组
用于存储多个指针,通常用于处理字符串数组或其他需要存储多个指针的情况。(字符串我们后续讲)数组指针
用于指向一个数组,常用于传递数组给函数,而无需关心数组的具体大小。
2-5 指针实现动态数组(进阶)
- (后面我们讲到
malloc
再说)
3指针与函数
3-1 传递指针给函数
- 我们来回顾一下函数的基本知识:
int func(int a)
{
a=10;
return a;
}
int main()
{
int a=20;
printf("func(a):%d\n",func(a));
printf("a:%d\n",a);
}
- 弹幕里头回答一下这两题的答案:
- 答案是
- func(a):10
- a:20
- 这是由于函数在进行传参时候,其实是进行了一次值拷贝,讲外面的a拷贝给了函数内的形参。
- 那么这时候如果我希望直接修改a的值而不进行
return
赋值给a,我们可以使用指针进行传参。
void func(int* a)
{
*a=10;
}
int main()
{
int a=20;
fun(&a);
printf("a:%d\n",a);
}
- 在这个修改后的例子中,当我们将
a
的地址(即&a
)传递给func
函数时,func
函数接收到的实际上是一个指向a
的指针。在func
函数内部,我们通过解引用这个指针(使用*
操作符)来直接访问并修改a
的值。因此,当func
函数执行完毕后,a
的值将被修改为10。 - 需要注意的是函数的参数是
int*
类型,因此传入参数的时候需要注意传入int*
的内容,因此需要取a的地址,保证类型一致。
3-1-1 swap
- 我们来看一个最经典的例子,
my_swap(int*a,int*b)
函数是一个经典的函数,用于交换a和b的值
#include <stdio.h>
// 交换两个整数的值
void my_swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10;
int y = 20;
printf("Before swap: x = %d, y = %d\n", x, y);
my_swap(&x, &y);
printf("After swap: x = %d, y = %d\n", x, y);
return 0;
}
- 我们使用
mermaid
过一下上述的流程
int temp = *a;
*a = *b;
*b = temp;
3-2 传递数组给函数
- 在C语言中,你可以将数组作为参数传递给函数。当你传递一个数组给函数时,实际上传递的是数组的指针。这是因为数组名在表达式中被视为指向数组第一个元素的指针。
#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 myArray[] = {1, 2, 3, 4, 5}; // 定义并初始化一个数组
int size = sizeof(myArray) / sizeof(myArray[0]); // 计算数组的大小
printArray(myArray, size); // 调用函数,传递数组及其大小
return 0;
}
3-2-1 函数传递到数组中无法计算数组大小问题
- 可能由同学看到上面的例子可能有疑问,为什么还需要额外传递数组的大小进入函数,为什么我们不能在函数内部动态计算数组的大小呢?
- 计算数组大小
sizeof(myArray) / sizeof(myArray[0]);
- 计算数组大小
- 实践出真知
void printArray(int* arr, int size)
{
printf("sizeof(myArray) / sizeof(myArray[0]):%zu\n", sizeof(arr) / sizeof(arr[0]));//输出2
printf("size:%d\n", size);//输出5
}
sizeof(arr) / sizeof(arr[0])
输出是指针的大小- 这是因为当你传递数组到函数时,你实际上传递的是数组的指针,而不是整个数组。
3-3 冒泡排序
-
说了这么多数组和函数指针的例子,我们来看看实际的运用需求
-
假如我们有个数组,数组内由杂乱无章的随机数据,我希望写一个函数,讲数组传入,把数组内元素按照
从小到大
的顺序进行排序。 -
这里我们简单介绍一下最简答的一个排序算法----
冒泡排序
-
冒泡排序
是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复进行的,直到没有再需要交换的元素为止。 -
假设我们由这样一个数组如下:
-
首先检查他们是否是按照从小到大的顺序:
-
如果是,交换他们的次序:
-
然后继续遍历下一次的两个数据
-
如果是,继续交换次序
-
上述过程一次类推,当一行的数据都完成排序后再次回到开头再次进行两两排序
-
代码实现如下:
#include <stdio.h>
void bubbleSort(int* arr, int size);
int main() {
int arr[] = { 64, 34, 25, 12, 22, 11, 90 };
int n = sizeof(arr) / sizeof(arr[0]);
printf("Before pass : ");
for (int k = 0; k < n; k++) {
printf("%d ", arr[k]);
}
printf("\n");
bubbleSort(arr, n);
printf("Sorted array: \n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
void bubbleSort(int* arr, int size) {
int i, j, temp;
for (i = 0; i < size - 1; i++) {
for (j = 0; j < size - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
// 交换 arr[j] 和 arr[j+1]
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
printf("After pass %d: ", i + 1);
for (int k = 0; k < size; k++) {
printf("%d ", arr[k]);
}
printf("\n");
}
}
}
- 这里我写了每一次交换的输出
3-3 函数返回值为指针(进阶)
- 函数返回值为指针意味着该函数的返回类型是指针类型。
int* func();
double func1();
3-3-1 局部变量指针返回报错,悬垂指针(dangling pointer)
- 需要特别注意的是如果返回的指针指向的是局部变量,那么在函数返回后,局部变量会被销毁,这会导致指针指向无效的内存地址。
- 我们来看一个例子
int* func()
{
int a=10;
int* p=&a;
return p;
}
- 上述代码毫无疑问会报错!!!
3-3-2 正确的使用:返回堆区开辟的指针
- 正确的应该这样做,应当返回指向动态分配内存(堆区)的指针。动态分配的内存将持续存在,直到被显式释放。使用
malloc
或calloc
函数可以在堆上分配内存,并返回指向该内存的指针。
int * func()
{
int*p =(int*)malloc(sizeof(int));
*p = 10;
return p;
}
3-4 函数指针和回调函数(进阶)
3-4-1 函数签名
- 在讲函数指针之前我们需要明确一个概念
函数签名(Function Signature)
是编程语言中用于描述函数接口的一个术语,它包括函数的名称、参数类型和数量、以及返回类型。函数签名不包含函数体,即函数的实现细节。
返回类型 函数名(参数类型1, 参数类型2, ...);
- 例子一个
add
函数
int add(int a,int b);
- 函数签名是
int(int, int)
,接受两个int
类型的参数,并返回一个int
类型的值。
3-4-2 函数指针
- 在C语言中,函数名确实代表了函数的地址。
函数指针
是指向函数的指针变量。函数指针可以用来保存函数的地址,并可以在需要时调用该函数。- 函数指针的定义方式与普通指针类似,但需要指定函数的返回类型和参数列表,也就是
函数签名
- 我们为上述
add
函数(函数签名为int(int,int)
),设计一个指针指向add
int (*funcPtr)(int, int);
fp = add;
- 然后我们可以正常调用add函数
fp(10,20);
3-4-3 回调函数
回调函数(Callback Function)
是一种常见的编程模式,它允许我们将函数作为参数传递给其他函数,以便在特定事件发生时被调用。- 我们来看下述例子:
#include <stdio.h>
// 定义一个回调函数类型
typedef void (*Callback)(int);
void processValue(int value, Callback callback)
{
if (value > 10) {
callback(value);
}
}
void printValue(int value)
{
printf("The value is: %d\n", value);
}
int main() {
int value = 15;
// 调用 processValue 函数,并传递 printValue 作为回调函数
processValue(value, printValue);
return 0;
}
processValue
函数接受一个整数和一个回调函数。如果传入的整数大于10,它就会调用这个回调函数。printValue
是一个简单的回调函数,它打印传入的值。
3-4-4 函数指针和回调函数历程----四则运算计算器
- 那么函数指针和回调函数有上面用处呢,提供一个统一的可替换的用户接口。
- 举个例子我们来看一个两位数的四则运算函数,我们可以对函数指针进行封装。
- 如下两位数的四则运算函数的签名都是
int(int,int)
,我们就可以设计一个统一的函数指针接口int (*funcPtr)(int, int);
- 这样用户在调用的时候就无需关注实现细节,直接替换实现即可
- 如下两位数的四则运算函数的签名都是
#include <stdio.h>
typedef int (*Operation)(int, int);
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)
{
if (b != 0) {
return a / b;
} else {
printf("Division by zero!\n");
return 0;
}
}
int calculate(int a, int b, Operation op)
{
return op(a, b);
}
int main()
{
int a = 10;
int b = 5;
printf("Addition: %d\n", calculate(a, b, add));
printf("Subtraction: %d\n", calculate(a, b, sub));
printf("Multiplication: %d\n", calculate(a, b, mul));
printf("Division: %d\n", calculate(a, b, div));
return 0;
}
- 学习过
面向对象
的同学可以就不陌生了,这是一种非常好的面向对象思想Operation
类型定义了一个接口,它描述了所有四则运算函数的共同特征:- 它们都接受两个
int
类型的参数并返回一个int
类型的结果 int (*Operation)(int, int);
typedef int (*Operation)(int, int);
是使用typedef
简化定义。
- 它们都接受两个
- 然后,每种运算实现了这个接口。通过
calculate
函数,可以传入任何符合Operation
接口的函数,这样calculate
函数就可以执行不同的运算,而不需要知道具体的实现细节。
4 指针与常量
4-1 常量const
- 在我们看常量指针和指针常量之前,咱们先回顾常量是啥
- 在C语言中,
常量
是一个在程序执行期间其值保持不变
的量。 - 常量可以分为不同的类型,包括
- 字面常量(如数字、字符串)
- 整数字面量:如
42
、0xFF
。- 你无法
10=1;
- 你无法
- 浮点数字面量:如
3.14
、2.718f
。 - 字符字面量:如
'A'
、'\n'
。 - 字符串字面量:如
"Hello, World!"
。
- 整数字面量:如
- 枚举常量
Enum
,- 枚举是一种用户定义的数据类型,它包含一组命名的整数常量。例如:
enum Color {RED, GREEN, BLUE};
- 枚举是一种用户定义的数据类型,它包含一组命名的整数常量。例如:
```- 宏定义常量
#define
,这里PI是一个宏定义常量,在预处理阶段会被替换为3.14159。#define PI 3.14159
- 通过
const
关键字声明的常量。
- 字面常量(如数字、字符串)
const
关键字用于声明一个变量为常量,这意味着在程序执行期间,该变量的值不能被修改。一般我们命名为大写变量
const int MAX_SIZE = 100;
- 在程序运行的全阶段,你无法修改
MAX_SIZE
的值!!!修改了会报错。
4-2 常量指针和指针常量
4-2-1 常量指针(Pointer to Constant):只读不可以修改
- 常量指针是指向常量的指针。这意味着,指针指向的内存地址中的值是常量,不能通过指针修改该值。声明一个常量指针的语法如下:
const type *ptr;
- 熟悉的说人话环节:还是货架的例子,你不能通过卡牌上的编号修改对应位置的货物!!!(给你的卡牌(指针)权限是只读不允许修改)
- 举例子:
int var = 10;
const int *ptr = &var;
*ptr = 20; // 错误,不能通过ptr修改var的值
//可以正常访问
printf("The value of var is: %d\n", *ptr);
4-2-2 指针常量(Constant Pointer):不能移情别恋,纯爱(不是)
-
这就是纯爱(迫真)
-
指针常量是指针本身是常量。这意味着,指针的地址是常量,不能改变指针所指向的地址。
type * const ptr;
- 一旦
ptr
被初始化指向某个int
变量,就不能改变ptr
指向的地址。例如:- 但是你还可以修改其中的数值
*ptr = 15;
- 但是你还可以修改其中的数值
int var = 10;
int * const ptr = &var;
ptr = &var2; // 错误,不能改变ptr指向的地址
//但是可以修改其中的数值
*ptr = 15;
4-2-3 同时是常量指针和指针常量
- 那么十分同时可以是纯爱(不是)且只读(保护数据)呢,当然有!!!!
- 你还可以创建一个同时是常量指针和指针常量的变量,这意味着指针指向的值和指针本身都是常量。声明这种类型的语法如下:
const type * const ptr;
- 这样不能通过指针修改指向的值,也不能改变指针指向的地址。
5 指针和结构体
5-1 结构体指针
5-1-1 定义
- 和正常的类型一样,结构体指针我们这样定义:
struct Student {
char name[50];
int age;
float score;
};
struct Student stu1 = {"张三", 20, 90.5};
struct Student *ptr = &stu1;
5-1-1 正常访问
- 如下,先解引用,然后当成正常的结构体进行访问
(*ptr).member
5-1-2 ->访问(推荐)
- 值得注意的是,结构体指针访问数据的时候除了和正常一样使用解引用,还可以使用
->
进行数据访问
ptr->member
- 箭头操作符
->
的优先级高于解引用操作符*
,这意味着ptr->member
等同于(*ptr).member
。但是,使用箭头操作符更加直观和简洁,特别是在处理结构体指针时。 - 说人话:这东西更方便!!!而且更好记忆。
- 例子:
#include <stdio.h>
struct Student {
char name[50];
int age;
float score;
};
int main() {
struct Student stu1 = {"张三", 20, 90.5};
struct Student *ptr = &stu1;
// 使用箭头操作符访问结构体成员
printf("姓名:%s\n", ptr->name);
printf("年龄:%d\n", ptr->age);
printf("分数:%f\n", ptr->score);
return 0;
}
5-2 typedef
简化结构体指针定义
- 在C语言中,使用
typedef
关键字可以为结构体定义一个新的名称,这样就可以简化结构体指针的定义。通过typedef
,你可以创建一个结构体类型的别名,使得在声明结构体变量或指针时更加简洁。
#include <stdio.h>
// 定义一个结构体类型
typedef struct {
char name[50];
int age;
float score;
} Student;
int main() {
// 定义一个结构体变量
Student stu1 = {"张三", 20, 90.5};
// 定义一个指向结构体变量的指针
Student *ptr = &stu1;
// 使用箭头操作符访问结构体成员
printf("姓名:%s\n", ptr->name);
printf("年龄:%d\n", ptr->age);
printf("分数:%f\n", ptr->score);
return 0;
}
5-3 结构体指针和函数
- 我们可以吧结构体指针传入函数以修改结构体内的数据
#include <stdio.h>
typedef struct {
int x;
int y;
} Point;
void modifyPoint(Point *p, int newX, int newY) {
p->x = newX;
p->y = newY;
}
int main()
{
Point point = {1, 2};
modifyPoint(&point, 10, 20);
printf("x: %d, y: %d\n", point.x, point.y);
return 0;
}
5-4 结构体内嵌指针(进阶)
- 在C语言中,结构体可以包含指针类型的成员。这种结构体称为内嵌指针的结构体。内嵌指针可以指向任何类型的数据,包括基本数据类型、数组、函数或其他结构体。
typedef struct {
int *ptr;
} PointerStruct;
- 我们可以正常使用
int value = 10;
PointerStruct ptrStruct;
ptrStruct.ptr = &value;
printf("值:%d\n", *ptrStruct.ptr);
5-4-1 结构体内嵌指针与二级指针
- 那么如果结构体本身使用指针进行访问呢,哎,就是套娃,那就是二级指针
typedef struct {
int *ptr;
} PointerStruct;
PointerStruct* ptrStruct;
*(ptrStruct->ptr)=10;
printf("值:%d\n", *(ptrStruct->ptr));
5-5 结构体内嵌结构体指针(进阶)
5-5-1 内嵌其他结构体(进阶)
- 结构体内嵌结构体指针是一种高级的数据结构设计技术,它允许你创建复杂的数据结构,如链表、树、图等。
- 通过在结构体中嵌入其他结构体的指针,你可以实现多层次的数据嵌套,从而创建出灵活且功能强大的数据结构。
- 例如我们举个游戏的例子:
typedef struct
{
int attackPower; // 武器的攻击力
char name[50]; // 武器的名称
} Weapon;
// 定义一个盔甲结构体
typedef struct {
int defensePower; // 盔甲的防御力
int durability; // 盔甲的耐久度
char name[50]; // 盔甲的名称
} Armor;
// 定义一个库存结构体
typedef struct {
Weapon* weapons[10]; // 玩家持有的武器数组
Armor* armors[10]; // 玩家持有的盔甲数组
int weaponCount; // 武器数量
int armorCount; // 盔甲数量
} Inventory;
// 定义一个玩家结构体
typedef struct {
Inventory* inventory; // 玩家的库存
char playerName[50]; // 玩家的名称
} Player;
- 通过结构体内嵌其他结构体,我们可以很好的封装一个对象(又是面向对象内容)
int main() {
// 创建一个武器实例
Weapon sword = {100, "剑"};
// 创建一个盔甲实例
Armor plateArmor = {50, 100, "板甲"};
// 创建一个库存实例
Inventory inventory;
inventory.weapons[0] = &sword;
inventory.armors[0] = &plateArmor;
inventory.weaponCount = 1;
inventory.armorCount = 1;
// 创建一个玩家实例
Player player;
strcpy(player.playerName, "玩家1");
player.inventory = &inventory;
// 使用玩家持有的武器
printf("%s 使用了 %s,攻击力为:%d\n", player.playerName, player.inventory->weapons[0]->name, player.inventory->weapons[0]->attackPower);
// 穿戴玩家持有的盔甲
printf("%s 穿戴了 %s,防御力为:%d\n", player.playerName, player.inventory->armors[0]->name, player.inventory->armors[0]->defensePower);
return 0;
}
5-5-2 结构体内嵌自身(进阶)
- 结构体内嵌自身是指一个结构体中包含一个指向相同类型的指针。这种设计通常用于实现链表、树等数据结构。
5-5-2-1 链表
- 链表是一种常见的数据结构,它由一系列节点(Node)组成,每个节点包含数据和指向下一个节点的指针。链表可以用于实现栈、队列、哈希表等高级数据结构。链表的主要特点包括:
- 动态大小:链表的大小不是固定的,可以在运行时动态地增加或减少节点。
- 插入和删除操作效率高:在链表中插入或删除节点通常只需要常数时间(O(1)),而不需要移动其他元素,这在数组中通常需要O(n)的时间。
- 非连续存储:链表中的节点可以存储在内存中的任何位置,节点之间的顺序由指针确定,而不是物理位置。
5-5-2-2 链表实现
Node
结构体定义了一个链表节点,其中next
成员是一个指向另一个Node
结构体的指针。
typedef struct {
int data;
struct Node *next;
} Node;
- 这里不多介绍了,感兴趣的同学了解了解即可。具体代码如下:
#include <stdio.h>
#include <stdlib.h>
// 定义一个链表节点结构体
typedef struct Node {
int data;
struct Node* next;
} Node;
// 函数声明
Node* createNode(int data);
void appendNode(Node** head, int data);
void printList(Node* head);
void freeList(Node* head);
int main() {
Node* head = NULL; // 链表头指针
// 向链表中添加节点
appendNode(&head, 1);
appendNode(&head, 2);
appendNode(&head, 3);
// 打印链表
printf("链表内容:\n");
printList(head);
// 释放链表内存
freeList(head);
return 0;
}
// 创建一个新的节点
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
fprintf(stderr, "内存分配失败\n");
exit(EXIT_FAILURE);
}
newNode->data = data;
newNode->next = NULL;
return newNode;
}
// 向链表末尾添加节点
void appendNode(Node** head, int data) {
Node* newNode = createNode(data);
if (*head == NULL) {
*head = newNode;
}
else {
Node* current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = newNode;
}
}
// 打印链表内容
void printList(Node* head) {
Node* current = head;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
// 释放链表内存
void freeList(Node* head) {
Node* current = head;
while (current != NULL) {
Node* temp = current;
current = current->next;
free(temp);
}
}
- 输出内容如下
5-6 结构体内嵌函数指针(进阶)
- 上节课前面的学长提到过了结构体内嵌函数,这是再C++的一个典型用法,但是再纯正的C语言是行不通的(大家也别怪前面的学长~讲错很正常)
- 正确的做法是内嵌函数指针
- 结构体可以内嵌函数指针,这使得结构体不仅能够存储数据,还能存储指向函数的指针,从而允许结构体在运行时执行特定的代码。这种能力使得结构体可以模拟
面向对象
编程中的对象和方法。
- 结构体可以内嵌函数指针,这使得结构体不仅能够存储数据,还能存储指向函数的指针,从而允许结构体在运行时执行特定的代码。这种能力使得结构体可以模拟
#include <stdio.h>
// 定义一个函数类型
typedef void (*FunctionType)(void);
// 定义一个结构体,包含一个函数指针
typedef struct {
int value;
FunctionType function;
} MyStruct;
// 定义一个函数,供结构体中的函数指针使用
void printValue(MyStruct *obj)
{
printf("Value: %d\n", obj->value);
}
int main() {
// 创建一个结构体实例
MyStruct myStruct;
myStruct.value = 10;
myStruct.function = printValue;
// 通过函数指针调用函数
myStruct.function(&myStruct);
return 0;
}
5-6-1 学过C++的同学(this
)
- 题外话,在C语言中,结构体本身并不存储
this
指针。实际上在C语言中,函数通常通过显式传递结构体指针来访问成员。
void printValue(MyStruct *obj)
{
printf("Value: %d\n", obj->value);
}
6 指针与字符和字符串
6-1 字符指针
- 字符指针是一个指向字符变量的指针。它可以用来访问和修改字符变量的值。
char ch = 'A';
char *ptr = &ch;
printf("字符: %c\n", *ptr); // 输出字符
6-2 字符数组(字符串)
- 字符串在C语言中通常是通过字符数组实现的。字符数组的最后一个元素是空字符(‘\0’),表示字符串的结束。
char str[] = "Hello";
char *ptr = str;
printf("字符串: %s\n", ptr); // 输出字符串
6-3 字符指针数组
- 字符指针数组是一个数组,其中每个元素都是一个指向字符的指针。它可以用来存储多个字符串。
char *arr[] = {"Hello", "World", "C Programming"};
printf("字符串: %s\n", arr[0]);
6-4 字符指针与字符串函数
- C标准库提供了许多处理字符串的函数,如
strlen
,strcpy
,strcat
,strcmp
等。这些函数通常接受字符指针作为参数
#include <string.h>
char str1[] = "Hello";
char str2[] = "World";
int len = strlen(str1); // 获取字符串长度
strcpy(str2, str1); // 复制字符串
strcat(str1, str2); // 连接字符串
int cmp = strcmp(str1, str2); // 比较字符串
6-5 动态分配字符串
- 使用
malloc
和calloc
函数可以动态分配内存来存储字符串。
#include <stdlib.h>
char *ptr = (char *)malloc(10 * sizeof(char)); // 分配内存
if (ptr != NULL) {
strcpy(ptr, "Hello"); // 复制字符串
printf("字符串: %s\n", ptr); // 输出字符串
free(ptr); // 释放内存
}
7 指针与文件I/O(进阶)
- 在C语言中,文件I/O操作通常涉及到文件指针(
FILE
类型指针),它是一个指向FILE
结构体的指针,该结构体定义了标准库中用于文件操作的各种信息。文件指针允许程序与文件进行读写操作,如打开文件、读取文件内容、写入数据到文件等。
7-1 打开文件
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
perror("Error opening file");
return -1;
}
7-2 读取文件内容
char buffer[1024];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
fclose(fp);
7-3 写入数据到文件
FILE *fp = fopen("example.txt", "w");
if (fp == NULL) {
perror("Error opening file");
return -1;
}
fprintf(fp, "Hello, World!");
fclose(fp);
- 后面的内容全是进阶辣,感兴趣的同学再继续学习吧
- 还想继续?那来吧!
8 指针与动态内存分配(进阶)
动态内存分配
是C语言中的一个高级特性,它允许程序在运行时从堆区(heap)
分配内存,而不是在编译时从栈区(stack)
分配。- 动态内存分配提供了更大的灵活性和控制能力,但也要求程序员负责管理内存的分配和释放,以避免内存泄漏和访问越界等问题。
8-1 程序四大区(进阶)
- 程序在执行时,其内存被划分为四个主要区域,分别是代码区、栈区、堆区和数据区。这些区域各自有不同的用途和生命周期。
- 代码区(Text Segment):
- 代码区存储程序的机器代码,即CPU执行指令的集合。
- 它通常包含函数定义、程序入口点等。
- 代码区是只读的,以防止程序在执行过程中修改自己的代码。
- 在程序的生命周期内,代码区的内容通常不会改变。
- 数据区(Data Segment):
- 数据区包含程序中定义的全局变量和静态变量。
- 全局变量是在所有函数外部定义的变量,它们在程序的整个执行期间都存在。
- 静态变量在函数内部定义,但使用
static
关键字,它们在程序的整个执行期间存在,并且只初始化一次。 - 数据区在程序开始执行前被初始化,并且在程序结束前一直存在。
- 栈区(Stack):
- 栈区用于存储局部变量、函数参数、返回地址等。
- 每当调用一个函数时,相应的信息(如返回地址、局部变量等)会被压入栈中。
- 当函数返回时,这些信息会被弹出栈。
- 栈是自动管理的,由编译器在函数调用时分配和释放内存。
- 栈的大小通常有限,并且是连续的。
- 堆区(Heap):
- 堆区用于动态内存分配,程序可以在运行时从堆区请求内存。
- 堆的大小通常比栈大得多,但它不是连续的,因此可以动态地增长和收缩。
- 动态分配的内存不会随着函数调用的结束而自动释放,程序员必须显式地使用
free
函数来释放它们。 - 堆区的管理是程序员的责任,不当的管理可能导致内存泄漏。
8-2 void*
的特性(进阶)
void*
是一个特殊的指针类型,它可以指向任何类型的数据。这意味着你可以将任何类型的指针赋值给void*
类型的指针,反之亦然。- 但是,当你将
void*
指针赋值给其他类型的指针时,必须进行显式类型转换。
int *ptr = (int *)malloc(sizeof(int)); // 从void*转换为int*
- 使用
void*
指针时,你需要记住它不保持任何关于它指向的数据类型的信息,因此在解引用之前必须进行正确的类型转换。
8-3 malloc
和free``(进阶)
malloc
函数用于从堆区分配指定大小的内存块,并返回一个指向该内存块的void*
类型的指针。如果内存分配失败,malloc
将返回NULL
。
void* malloc(size_t size);
free
函数用于释放之前通过malloc
、calloc
或realloc
分配的内存。当你不再需要动态分配的内存时,应该使用free
函数来释放它,以避免内存泄漏。
void free(void *ptr);
- 普通数据的动态分配
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int)); // 动态分配一个int类型的内存
if (ptr == NULL) {
fprintf(stderr, "内存分配失败\n");
return 1;
}
*ptr = 10; // 给分配的内存赋值
printf("值: %d\n", *ptr); // 输出值
free(ptr); // 释放分配的内存
return 0;
}
- 数组的动态分配
#include <stdio.h>
#include <stdlib.h>
int main() {
int n = 5;
int *array = (int *)malloc(n * sizeof(int)); // 动态分配一个包含5个int的数组
if (array == NULL) {
fprintf(stderr, "内存分配失败\n");
return 1;
}
for (int i = 0; i < n; i++) {
array[i] = i * i; // 给数组赋值
}
// 输出数组
for (int i = 0; i < n; i++) {
printf("%d ", array[i]);
}
printf("\n");
free(array); // 释放分配的内存
return 0;
}
- 二维数组的动态分配
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 3;
int cols = 4;
int **array2D = (int **)malloc(rows * sizeof(int *)); // 动态分配行指针
if (array2D == NULL) {
fprintf(stderr, "内存分配失败\n");
return 1;
}
for (int i = 0; i < rows; i++) {
array2D[i] = (int *)malloc(cols * sizeof(int)); // 动态分配每行的列
if (array2D[i] == NULL) {
fprintf(stderr, "内存分配失败\n");
// 释放之前已分配的行
for (int j = 0; j < i; j++) {
free(array2D[j]);
}
free(array2D);
return 1;
}
}
// 给二维数组赋值
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
array2D[i][j] = i * cols + j;
}
}
// 输出二维数组
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", array2D[i][j]);
}
printf("\n");
}
// 释放二维数组的内存
for (int i = 0; i < rows; i++) {
free(array2D[i]);
}
free(array2D);
return 0;
}
9 C语言编译冷知识(进阶)
- 能看到这里的你,已经很厉害了~
- 让 我们回到梦开始的地方
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
- 你是否好奇,你的一段简单的C语言程序,点击运行按钮的时候,到底做了什么呢?
- 当你编写了一段C语言程序并点击运行按钮时,计算机实际上执行了一系列复杂的步骤来将你的源代码转换成可执行的机器代码。这个过程通常被称为编译过程,它大致可以分为四个阶段:预处理、编译、汇编和链接。
9-1 预处理
- 预处理阶段是编译过程的第一步,由预处理器(如C语言中的
cpp
)处理。在这个阶段,预处理器会执行以下操作:- 展开所有的宏定义。
- 处理所有的文件包含指令(
#include
),将指定文件的内容插入到当前位置。 - 移除所有的注释。
- 根据指令(如
#define
)替换所有的宏。 - 条件编译指令(如
#ifdef
、#ifndef
、#if
、#endif
)的控制。
- 预处理后的代码通常会有显著的增长,因为它包含了所有被包含的文件内容。
9-2 编译
- 编译阶段是由编译器(如GCC的
cc1
)执行的。在这个阶段,编译器会将预处理后的代码转换成汇编语言。这个过程包括:- 词法分析:将源代码分解成一系列的记号(token)。
- 语法分析:检查记号序列是否符合C语言的语法规则,并构建抽象语法树(AST)。
- 语义分析:检查AST的语义正确性,如类型检查。
- 代码生成:将AST转换成汇编语言代码。
9-3 汇编
- 汇编阶段是由汇编器(如GCC的
as
)执行的。汇编器将汇编语言代码转换成机器代码。这个过程包括:- 将汇编语言指令转换为机器指令。
- 分配内存地址给各个指令和数据。
- 生成可重定位的目标文件(
.o
文件),其中包含机器代码和数据。
9-4 链接
- 链接阶段是由链接器(如GCC的
ld
)执行的。链接器将一个或多个目标文件和所需的库文件合并,生成最终的可执行文件。这个过程包括:- 合并各个目标文件中的代码和数据。
- 解析外部引用,确保所有函数和数据项都能正确链接。
- 设置程序的入口点。
- 生成可执行文件(通常是
.exe
或.out
文件)。
9-5 C语言编译器
- C语言编译器是一种将C语言源代码转换成机器语言(即二进制代码)的工具,以便计算机可以理解和执行。
- 常用的C语言编译器包括:
- GCC(GNU Compiler Collection):这是一个开源的编译器集合,支持多种编程语言,包括C、C++、Objective-C、Fortran、Ada、Go等。GCC是Linux系统下最常用的编译器。
- Clang:由苹果公司开发的编译器,与GCC兼容,但通常更快且更易于调试。
- MSVC(Microsoft Visual C++):微软开发的编译器,主要用于Windows平台。
- ICC(Intel C++ Compiler):由英特尔开发,针对英特尔处理器进行了优化。
10 Cmake和CmakeList和Makefile(进阶)
CMake
是一个跨平台的安装(编译)工具,它可以用来管理软件的编译过程。它使用一个名为CMakeLists.txt
的文件来编写构建系统的描述,该文件描述了软件项目的构建规则和依赖关系。CMake可以生成不同平台上的本地构建文件,如Makefile、Visual Studio的项目文件等。CMakeLists.txt
文件是CMake的核心配置文件,它包含了构建项目的所有指令。例如,你可以指定源文件、头文件、库依赖、编译器标志、安装路径等。CMake会根据CMakeLists.txt
文件和CMake的内置规则来生成对应平台的构建文件。Makefile
是一个文件,包含了用于编译源代码并生成可执行文件或库文件的指令。它是由GNU Make程序读取和执行的。Makefile通常由手动编写,也可以由其他工具生成,如CMake。- CMake与Makefile的区别在于,CMake提供了一个更高层次的抽象,可以自动处理不同平台的构建细节,而Makefile则更接近于构建过程的底层细节。使用CMake,你可以更容易地为多个平台创建构建系统,因为它会根据你的
CMakeLists.txt
文件和系统的CMake模块来生成相应的Makefile。
总结-完结撒花
- 自此我们的C语言培训课程就到这里结束了
- 感谢大家一路一来的坚持和努力,同时感谢各位学长学姐的培训!