简介
这一部分,是C语言的重中之重。学习完C语言之后,懂得原理比记得住怎么写代码更重要。因为后面写代码很少遇到C语言(目前用到C的地方是机器人电控代码编写)。但是原理是你精通C++、Java等的基础。
这些内容,如果今天不讲我真的也不会去细看,也算一起进步了。
一些前置知识
存储分类
速度排序:寄存器(register)、L1、L2、L3(CPU三级缓存)、内存、外存(固态)
存储单位
最小的存储单位:比特(bit),表示一个二进制位
规定:1个字节由8个比特位表示,原因和历史发展有关系,这也是char类型由来:
在计算机科学发展的早期,8位(bit)被选为处理字符的合适大小。当时,大多数计算机系统是基于8位的,因为8位足以表示所有的英文字母(大写和小写)、数字、以及其他常用的符号(如标点符号),这些字符集后来被称为ASCII码。
位:英文名称叫做”bit”,表示一个二进制位。32位就是用32个bit表示内存地址,64位就是用64个bit表示内存地址。因此可以简单理解为:”位数=寻址能力”。
内存地址
操作系统为了管理内存,将内存每一个字节编上号,这样每一个号码我们称为内存地址。
内存地址=门牌号:也是一个整数(比如0001、0002……),这个整数有多少就表示操作系统能管理多少内存。
为了方便表示(十进制太长了),我们惯用十六进制表示内存地址,例如”0x5FFE8C“:0x表示这是个十六进制的数字。
32位与64位
内存地址由整数表示,那这个整数有没有最大上限呢?实际上,这个上限就是计算机能管理的最大内存。要找到这个上限,其实我们只需要知道计算机能够表示的最大整数就行了。
两者最大支持内存
知道了位的概念,就可以作出如下计算:
- 32位:可以寻址232个不同的地址,即4GB大小的内存。
- 64位:可以寻址264个内存地址,即16EB(1EB = 1024PB,1PB=1024TB,1TB=1024GB)的内存。
一些俗称
- x86:x86或80×86是英特尔Intel首先开发制造的一种微处理器体系结构的泛称,为32位系统,因此一般在作为32位系统的代名词。
- amd64:x64于1999年由AMD设计,AMD首次公开64位集以扩展给x86,称为“AMD64”,因此一般作为64位系统的代名词。
C语言内存模型
C/C++和Java的区别:C/C++可以直接操作系统内存、手动回收、手动释放内存,Java:不能直接操作系统内存(Java代码运行在JVM中,Java Virtual Machine),自动管理内存
Tips:重点掌握堆区、栈区。这一部分到数据结构你们会学,谁先学到谁薄纱。
栈区
定义
存放函数的参数值、局部变量等,由编译器自动分配和释放,通常在函数执行完后就释放了,其操作方式类似于数据结构中的栈。
底层实现
- 一段连续的存储空间
- 数据结构的栈(数组实现方式)
声明方法
省流:你代码中没有指针的变量、定长的数组就是位于栈区的,如下代码所示:
int num = 20;
int nums[20];
上面这两种方式声明的变量存储的位置就在栈区,为什么?因为大小是固定的,运行时无法改变,编译器在编译的时候就确定了。
特点
- 静态:在栈区申请空间的变量都是预先由开发者设定好大小的,编译器分配,程序运行时无法改变。
- 快速:底层实现为数据结构的栈,类似于受限制的数组,由CPU直接管理,非常适合数据的快速读写(例如函数的调用栈也是一个道理)
- 容量小:栈区空间非常小过大会发生栈溢出,常见错误为:段错误(segmentation fault)或者是缓冲区溢出(Buffer overflow)
堆区
就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放工作,需要用程序区释放。分配方式类似于数据结构中的链表。“内存泄漏”通常说的就是堆区。
#include <stdlib.h>
int* ptr = (int*)malloc(sizeof(int) * number_of_elements);
if (ptr == NULL) {
// 处理内存分配失败的情况
exit(EXIT_FAILURE);
}
// 使用分配的内存
// 用完后记得释放内存
free(ptr);
堆区的特点:排序二叉树、链表存储(高效利用空间、但是读取速度满(需要寻址)、消耗内存多(需要存储每一个元素的内存地址))
静态区
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后,由系统释放。
常量区
常量存储在这里,不允许修改。
代码区
顾名思义,存放代码。
指针与变量
很多人会说,指针就是内存地址,其实我之前也是这么理解的,实际上是错的。
在C语言中,指针的理解应该为:指针=内存地址+数据类型。
定义指针变量(int*)
阅读下列的代码:
int *pointer_1,*pointer_2;
这两行代码声明了pointer_1和pointer_2是两个指针变量(存放指针的变量)。
取出一个变量的地址(&)
阅读下列的代码:
int var1 = 1,var2 = 2;
int *pointer_1 = &var1,*pointer_2 = &var2;
简单看看,”&”就是取出变量的内存地址,并将地址赋值给指针变量。但实际上这个理解有问题。
让我们对这个代码做一下修改:
int var1 = 1,var2 = 2;
double *pointer_1 = &var1,*pointer_2 = &var2;
对代码进行编译,就会报下面的错误:
error: initialization of ‘double *’ from incompatible pointer type ‘int *’ [-Wincompatible-pointer-types]
如果单单是地址,那么只要是指针变量,都应该能接收,根据报错来看,”&”返回的指针既包括了内存地址,也包括了数据类型。
在C语言中,一个变量的指针的含义包括两个方面,一是以存储单元编号表示的内存地址(如编号为2000的字节),一是它指向的存储单元的数据类型(如int,char,float等)。
访问指针(*p)
*:指针运算符(也叫”间接访问”运算符),*p代表指针变量指向的对象。
int a = 1; // 定义一个变量a
int *p = &a; // 取a的地址并用指针变量p存储
*p = 3; // 使用"*"操作符("间接访问"运算符)修改p指向的变量a的值为3
所以有个经典的问题:我能否用int存放指针并操作对应的变量?
答案显然,是不行的。在上文中你可以发现,C语言的指针并不是单一的内存地址,而是包含了数据类型。但是,你可以将一个指针作为整数输出,直观感受什么是指针。(也就是说,这个过程是不可逆的)
#include <stdio.h>
#include <stdlib.h>
int main(){
int value = 10;
int *ptr = &value; // ptr指向value的地址
int *arr = (int*)malloc(sizeof(int)*13);
// 输出指针变量的地址(即ptr存储的值)
printf("Pointer address: %X\n", ptr);
printf("Pointer address: %X\n", arr);
return 0;
}
指针与二维数组
sizeof运算符
在C语言中,sizeof运算符用于确定变量或数据类型的大小(以字节为单位)。sizeof是一个编译时运算符,它的结果是由所使用平台的字节大小决定的,计算是通过编译器计算的,而不是通过运行后测量的。
int arr[10];
printf("Size of array arr: %zu bytes\n", sizeof(arr));
| 数据类型 | 大小(字节) | 比特位 |
| char | 1 | 8 bit |
| int | 4 | 32 bit |
| float | 4 | 32 bit |
| double | 8 | 64 bit |
| long long int | 8 | 64 bit |
数组的本质
在C中声明一个数组,实际上这个标识符只记录了数组的第一个元素的指针。
数组是一组有序数据的集合,其存储方式是连续的,也就可以通过计算确定每个元素的指针位置并操作它。
指针的运算
指针的加法与减法,实际上是在内存地址的基础上,加上对应数据类型的整数倍。例如下面的代码:
int a[10];
*(a+2)=5; //将a中第三个元素赋值为5
a[2]=5; //这两个写法是等效的
等效原因:[]实际上是变址运算符,即将a[i]按a+i计算地址,然后找出此地址单元中的值。这里a+2运算过后得到的新指针地址为:a的首元素内存地址+sizeof(int)*2,数据类型不变,为int。
因此访问数组元素有两种方法:
- 下标法:a[i]
- 指针法:*(a+i)或*(p+i)
动态内存分配
根据程序运行状态,动态申请空间,返回的均为首元素的指针。
malloc:申请对应大小的空间
calloc:申请n个对应大小的空间并初始化为0
realloc:重新分配动态存储区。
int* ptr1 = (int*)malloc(10 * sizeof(int)); // 申请10个整型的空间
int* ptr2 = (int*)calloc(10, sizeof(int)); // 申请10个整型的空间并初始化为0
int* ptr3 = (int*)malloc(5 * sizeof(int)); // 初始分配5个整型的空间
int* new_ptr = (int*)realloc(ptr3, 10 * sizeof(int));// 调整内存大小
// 用完了记得释放空间
free(ptr1);
free(ptr2);
free(new_ptr);
字符串
ASCII
在C语言中,字符用char表示,一个字符相当于一个整数,对应的为ASCII表(附录A):

// 一般来说,用char存放字符
char c = 'a';
// 由于字符相当于一个整数,当然也可以通过int存放
int c = 'a';
可以记住几个特殊值,后面用起来方便
| ASCII | 字符 |
| 48 | ‘0’ |
| 65 | ‘A’ |
| 97 | ‘a’ |
认识字符串
C语言中没有字符串类型,也没有字符串变量,字符串是存放在字符型数组中的。
char c[]={"China"};
printf("%s\n",c);
这段代码中,c的实际存储情况如下:

这里c的长度编译器自动计算了,得出的应该是6。为什么是6呢?因为结尾还有个”\0″,如果使用printf输出,就是识别到”\0″结束输出的,如果没有”\0″,会输出一堆乱码。
你说我非要操作char[9]或者char[10]会怎么样?Java等高级编程语言编译就会报错导致不通过,但是C无所谓,这也就给黑客带来了机会:”缓冲区溢出攻击“
字符串相关函数
格式化(sprintf、fprintf、printf)
sprintf:字符数组fprintf:文件流(可以是标准输出、文件等)printf:标准输出(通常是屏幕)
可以用sprintf把信息输出到字符串,用法和printf、fprintf类似。但应当保证字符串足够大,可以容纳输出信息。
sprintf的用法:输出到字符串
#include <stdio.h>
int main() {
char buffer[100];
int num = 42;
sprintf(buffer, "The answer to life, the universe, and everything is %d.", num);
// buffer now contains the formatted string
// Use another function to print the buffer, or use it in another context
printf("%s\n", buffer); // This line is just to demonstrate the content of buffer
return 0;
}
fprintf的用法:输出到文件
#include <stdio.h>
int main() {
FILE *file = fopen("output.txt", "w"); // Open file for writing
if (file == NULL) {
perror("Error opening file");
return 1;
}
int num = 42;
fprintf(file, "The answer to life, the universe, and everything is %d.\n", num);
fclose(file); // Close the file
return 0;
}
printf的用法:输出到终端(略)
输入与输出(puts、gets、getchar)
- puts(字符数组):输出一个字符到终端。
- gets(字符数组):输入一个字符串到字符数组。(现在常用getchar)
gets和getchar:
gets函数用于读取一行文本直到遇到换行符(‘\n’)或EOF(文件结束标志),然后它会丢弃换行符,并用空字符(‘\0’)替换它,作为字符串的结束标志。gets函数会将读取的字符串存储在由其参数指定的字符数组中。getchar函数用于读取下一个可用的字符,通常是用户输入的第一个字符,并返回读取的字符作为int类型的值。如果遇到文件结束或读取错误,则返回EOF。
注意:
gets函数是不安全的,因为它不检查目标缓冲区的大小,可能会导致缓冲区溢出,这是一个严重的安全漏洞。因此,在新的代码中不应使用gets函数。getchar是安全的,因为它一次只读取一个字符,不会导致缓冲区溢出。
getchar读取整个文本(包括空格回车),核心是判断是否到EOF(End of File,为一个-1的常量):
#include <stdio.h>
int main() {
char ch;
printf("Enter your article (press Ctrl+D or Ctrl+Z to end):\n");
// 循环读取字符直到EOF
while ((ch = getchar()) != EOF) {
putchar(ch); // 将读取的字符输出到标准输出
}
return 0;
}
操作类函数(strcat、strcpy、strncpy、strcmp、strlen、strlwr、strupr)
参阅P158,用到再查,没必要一次性记那么多……
strlen:测出的长度不包括”\0“
杂七杂八的
还有啥没讲
- 整数的原码、反码与补码表示
- 从堆区申请空间,实现动态数组(要考)
- 指针与多维数组
- 全局变量与局部变量、变量存储位置
- 指针与函数、函数指针的应用
- void* 等
- 二重指针、多重指针
建议怎么学
先学C:老老实实地把我说的这些原理学透就行,语法后面忘记了都是无所谓的,但是得记住原理。
然后上C++/Java:两个对比学习
最后你就发现Python、PHP、JavaScript不用学了
想要深究计算机原理:推荐去学习汇编语言
参考
- 书籍名称:C语言程序设计(第五版)——谭浩强、算法竞赛入门经典(紫书)
- 章节名称:第6章 利用数组处理批量数据
[整理] 浅谈堆、栈、堆区、栈区的概念和区别 – 哆啦梦乐园 – 博客园 (cnblogs.com)
浅析C语言中的内存模型我正在参加「掘金·启航计划」 前言 近来学习深觉自己在语言的底层方面理解欠缺, 正好之前有看过一点 – 掘金 (juejin.cn)







