C语言八股文总结
关键字
部分内容参考
https://www.nowcoder.com/issue/tutorial?zhuanlanId=0rOeJm&uuid=8b0d1707bb0f43e0a6ce382773efc7d9
extern的作用
extern主要用于声明外部实体(变量或函数)
1.当一个全局变量在 A 文件中定义,需要在 B 文件中使用时,B 文件中需用 extern 声明该变量,表明它的定义在其他地方。
2. 声明外部函数(函数默认具有外部链接属性(可以被其他文件调用),因此 extern 修饰函数时可以省略)
static的作用
1.static的第一个作用是隐藏。(static函数,static变量均可)
当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
2.static的第二个作用是变量只初始化一次,保持变量内容的持久。
3.static的第三个作用是默认初始化为0
其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00。
volatile的作用
volatile 的核心作用是阻止编译器对变量访问的优化,确保每次读写都直接操作内存,适用于变量值可能被程序外部因素(硬件、中断、其他线程)异步修改的场景。
使用场景:硬件寄存器映射、中断服务程序中修改的变量、多线程共享的变量。
注意事项
1.volatile 仅保证每次访问都从内存读写,但不保证原子性(如 i++ 这类操作仍可能被打断)。
2.volatile 不能替代锁(mutex)或其他同步机制,复杂的多线程同步仍需专门的同步工具。
3.不要滥用 volatile:不必要的使用会降低程序效率(避免了编译器优化)。
const的作用
用于定义常量,表示变量的值不可修改。
使用场景:定义常量值、常量指针、指针常量、函数参数中的const、硬件寄存器的只读访问
const 与define区别
define既可以替代常数值,又可以替代表达式,甚至是代码段,但是容易出错,而const的引入可以增强程序的可读性,它使程序的维护与调试变得更加方便。
1.编译器处理方式不同
define宏是预编译指令,在预处理阶段展开。
const常量是普通变量的定义,编译运行阶段使用。
2.存储方式不同
define定义的是常量,仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。
const定义的是常变量,常量会在内存中分配(可以是堆中也可以是栈中),const 可以节省空间,避免不必要的内存分配。
const定义的常变量本质上仍然是一个变量,具有变量的基本属性,有类型、占用存储单元。可以说,常变量是有名字的不变量,而常量是没有名字的。有名字就便于在程序中被引用,所以从使用的角度看,除了不能作为数组的长度,用const定义的常变量具有宏的优点,而且使用更方便。所以编程时在使用const和define都可以的情况下尽量使用常变量来取代宏。
3.类型和安全检查不同
define宏没有类型,不做任何类型检查,仅仅是展开。容易出问题,即“边际问题”或者说是“括号问题”。
const常量有具体的类型,在编译阶段会执行类型检查。
4.const可以调试
const 只读变量是可以进行调试的,define 是不能进行调试的,因为在预编译阶段就已经替换掉了。
typedef与define区别
typedef 是 “类型别名定义工具”,由编译器保证类型一致性;#define 是 “文本替换工具”,功能更杂但缺乏类型检查,需谨慎使用
- 本质与处理阶段不同
typedef:是 C 语言的关键字,由编译器处理,属于语言层面的语法。
#define:是预处理指令,由预处理器在编译前(预处理阶段)处理,属于文本替换工具。 - 功能范围不同
typedef:仅用于为类型创建别名,不能定义常量、表达式或函数。
#define:可定义常量、宏函数、类型别名等。 - 类型检查不同
typedef:会进行类型检查,因为它定义的是 “真实的类型别名”,编译器会将其视为原类型的等价物。
#define:仅做文本替换,不进行类型检查,可能导致隐蔽的错误。 - 对指针和复杂类型的处理不同
typedef:为指针类型定义的别名是 “完整的类型”,声明变量时所有变量都会被视为该指针类型。
#define:对指针的处理是简单替换,可能导致部分变量类型不符合预期。
typedef int* PINT; // 定义 PINT 为 int* 的别名
PINT a, b; // 等价于 int* a, int* b; (a 和 b 都是 int 指针)
#define PINT int* // 宏替换:PINT 替换为 int*
PINT a, b; // 替换后为 int* a, b; (a 是 int 指针,b 是 int 变量)
- 作用域不同
typedef:有作用域限制,遵循 C 语言的作用域规则(如在函数内定义的 typedef 仅在该函数内有效)。
#define:没有作用域限制,从定义处开始到文件结束(或被 #undef 取消)均有效。 - typedef 常用于简化结构体 / 枚举的声明,而 #define 无法实现同等效果:
宏定义,输入两个参数并返回较小的一个
#define Min(X, Y) ((X)>(Y)?(Y):(X))
宏定义,不用中间变量,实现两变量的交换
#define swap(x,y) \
x=x+y;\
y=x-y;\
x=x-y //注意:结尾没有分号
指定位置1与清零
#define BIT3 (0x1<<3)
static int a;
void set_bit3(void)
{
a |= BIT3;
a |= 0x1<<3;
}
void clear_bit3(void)
{
a &= ~BIT3;
a &= ~(0x1<<3);
}
指定位反转
int mask = (1<<2) | (1<<4) | (1<<5);
a ^= mask;
访问硬件寄存器
#define rBANKCON0 (*(volatile unsigned long *)0x48000004)
rBANKCON0 = 0x12;
(1)由于是寄存器地址,所以需要先将其强制类型转换为 ”volatile unsigned long *”。
(2)由于后续需要对寄存器直接赋值,所以需要解引用。
指针
常量指针
指向一个常量的指针,这个指针无法修改所指向的数据,但是可以修改指向
const int* p= &a;
指针常量
指针是一个常量,指针所指向的地址是固定的,但是可以修改地址中的值
int* const pl = &a;
函数指针
指向一个函数的指针
int (*ptr)(int, int)= &add;
指针函数
一个函数的返回值是一个指针的函数
int* a(void)
{
}
指针的大小
指针的大小和编译器的位数有关,在32bit的系统下指针的大小是4个字节,在64bit的系统下指针是8个字节
数组名和指针的区别
1.数组名就是数组首元素的地址,也可以看作一个常量指针,这个指针是不能修改指向的,内存访问为4个字节。
2.使用指针访问数组的时候需要使用到解引用使用*,使用指针访问数组是间接访问,使用数组名访问数组是直接访问。
3.使用sizeof对指针和数组名进行计算的时候是不同的,指针的大小和编译器的仪数有关,使用sizeof计算数组名是整个数组的大小。
指针的指针用途
1.动态分配内存:使用指针的指针可以动态分配内存并将其传递给函数,以便函数可以修改指向该内存的指针。这样可以避免在函数中使用全局变量,提高程序的模块化程度。
#include <stdio.h>
#include <stdlib.h>
void allocate_memory(int** pptr) {
*pptr = (int*)malloc(sizeof(int));
**pptr = 10;
}
int main() {
int* ptr;
allocate_memory(&ptr);
printf("%d\n", *ptr);
free(ptr);
return 0;
}
2.动态创建多维数组(如二维数组)
int main() {
int rows = 3, cols = 4;
int** arr; // 二级指针:指向 int* 类型的数组(每行的首地址)
// 第一步:为“行指针数组”分配内存(存储每行的地址)
arr = (int**)malloc(rows * sizeof(int*));
// 第二步:为每行分配内存(每行大小可不同)
for (int i = 0; i < rows; i++) {
arr[i] = (int*)malloc(cols * sizeof(int));
arr[i][0] = i; // 给每行第一个元素赋值
}
// 使用二维数组
printf("%d", arr[2][0]); // 输出 2
// 释放内存(先释放每行,再释放行指针数组)
for (int i = 0; i < rows; i++) free(arr[i]);
free(arr);
return 0;
}
3.处理指针数组
void print_strings(char** strs, int count) {
for (int i = 0; i < count; i++) {
printf("%s\n", strs[i]); // strs[i] 是第 i 个字符串的地址(char*)
}
}
int main() {
char* fruits[] = {"apple", "banana", "cherry"}; // 指针数组(char* 数组)
print_strings(fruits, 3); // 传递指针数组名( decays 为 char** 类型)
return 0;
}
绝对地址或者访问硬件指定内存下数据
1.如何对绝对地址0x100000赋值?
*(unsigned int*)0x100000 = 1234;
2.读硬件寄存器地址数据
1.#define GPA1DAT (*(volatile uint32_t*)0xE0200084)
将 GPA1DAT 宏定义为地址 0xE0200084 上的内容
2.(volatile uint32_t*)0xE0200084
将0xE0200084强制转换为地址int型指针(相当于*p中的p,指的是地址)
3.(*(volatile uint32_t*)0xE0200084)
这句代码则代表地址为0xE0200084上的存放内容(相当于*p,指的是地址上的内容)
4.#define GPA1DAT (*(volatile unsigned int*)0xE0200084)
将地址0xE0200084上的内容定义为GPA1DAT,如果操作GPA1DAT = 1;则地址0x40000000上存放的内容就变成了1,也可以读.
3.那么要是想让程序跳转到绝对地址是0x100000去执行,应该怎么做?(万能函数指针)
*((void (*)( ))0x100000)( ); //((void(*)())p)(); //或者*(void(*)())p()
首先要将0x100000强制转换成函数指针,即:
(void (*)())0x100000
然后再调用它:
*((void (*)())0x100000();
用typedef可以看得更直观些:
typedef void(*)() voidFuncPtr;
*((voidFuncPtr)0x100000)();
或者这样写
void (*func_ptr)() = (void (*)(void)) 0x100000;
func_ptr();
内存

#include <stdio.h>
const int g_A = 10; //常量区
int g_B = 20; //数据段
static int g_C = 30; //数据段
static int g_D; //BSS段
int g_E; //BSS段
char *p1; //BSS段
int main()
{
int local_A; //栈
int local_B; //栈
static int local_C = 0; //BSS段(初值为0 等于没初始化,会放在BSS段 )
static int local_D; //BSS段
char *p3 = "123456"; //123456在代码段,p3在栈上
p1 = (char *)malloc( 10 ); //堆,分配得来得10字节的区域在堆区
char *p2 = (char *)malloc( 20 ); //堆上再分配,向上生长
strcpy( p1, "123456" );//123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一块
堆和栈的区别
1.创建方式不同,栈是系统自动创建(栈主要用于保存局部变量),当函数执行完成,栈被销毁
堆是程序员手动进行创建和释放的,malloc进行创建,free进行释放
2.空间大小的区别,栈的空间是比较小的,堆的空间是比较大
3.访问速度,栈的访问速度是比堆要快的
4.栈当使用完成后就会自动被销毁,堆是要靠程序员自己进行手动销毁
内存分配方式
1.静态存储器分配,例如全局变量,静态变量
2.栈上分配(函数中定义出的局部变量)
3.堆上分配(malloc, new)
在局部数组中定义一个大数组可以吗?很大的数组,比如2048
不可以,会爆栈,栈溢出
解决方法,(解决局部大数组爆栈和局部作用域的问题)
1.定义成全局数组
2.加static放在静态存储区
3.数组用malloc申请空间放在堆区
定义一个指针指向这个数组,栈中只占用一个指针的大小
char *fun()
{
char *a = (char*)malloc(sizeof(char)*100);
a = "hello,world";
return a;
}
内存泄露
一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
常见的内存泄露方式
1.未配对、2.丢失地址、3.未分级释放
char *pt = (char *)malloc(10); //堆上申请空间,未配对free(pt)
char *pt= (char *)malloc(10);
pt= (char *)malloc(20); //覆盖了指针,导致前面10个空间的地址丢失。
#include <stdio.h>
#include <stdlib.h>
struct birth
{
int year;
int month;
int day;
};
struct student
{
char is_male;
char *name;
struct birth * bi;
};
int main()
{
struct student *pt;
pt= (struct student *)malloc(sizeof(struct student)); //堆上申请空间
pt->is_male =1;
pt->name ="wangwei";
pt->bi = (struct birth *)malloc(sizeof(struct birth)); //堆上申请空间
pt->bi->year =2000;
pt->bi->month =3;
pt->bi->day =2;
printf("%s %d \n",pt->name,pt->bi->day);
//逐级释放空间,避免内存泄漏
//pt->name 是字符串常量 不用释放
if(pt->bi!=NULL){
free(pt->bi); //先释放子空间
pt->bi=NULL;
}
free(pt); //后释放父空间
pt =NULL; //避免野指针 (操作已释放的空间)
return 0;
}
避免内存泄露的几种方式
显式释放内存:程序在使用动态分配的内存时,应该及时使用free函数将不再需要的内存释放掉。
避免重复分配内存:程序在使用动态分配的内存时,应该避免重复分配内存,特别是在循环中。如果需要多次分配内存,可以使用realloc函数重新调整内存块的大小,以减少内存碎片的产生。
使用智能指针:智能指针是一种自动管理内存的工具,可以避免手动释放内存的繁琐操作。智能指针会在对象不再被使用时自动释放内存,并且可以避免内存泄漏和悬空指针等问题。
计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露。
字节对齐
字节对齐是指数据在内存中的起始地址必须是某个值的倍数(通常是2、4、8等)。
为什么需要字节对齐?
1.性能原因
CPU访问对齐的数据通常只需要一次内存访问操作
非对齐的数据访问可能需要多次内存访问,然后进行拼接操作
非对齐访问可能导致性能下降
2.硬件限制
某些处理器架构(如ARM、RISC-V)要求特定类型的数据必须对齐访问,
非对齐访问在某些平台上会导致硬件异常或错误
3.缓存效率
对齐的数据可以更好地利用CPU缓存行(cache line)
非对齐的数据可能跨越多个缓存行,降低缓存效率
结构体对齐规则
1.结构体的对齐要求是其所有成员中最大对齐要求的倍数
2.结构体的大小必须是其对齐要求的整数倍
3.成员按照声明顺序存储,但编译器可能会插入填充字节以满足对齐要求
其他
struct结构体 和 union联合体的区别
union 联合体:成员共享一块地址。共用体大小= 成员中占内存最大的成员的大小。
struct 结构体:不同的成员放在不同的地址中。结构体大小 = 所有成员大小之和(字节对齐)。
enum 枚举 :定义一组有名字的整数常量
编译过程

定义一个指针数组,指针指向一个函数,返回值为int,函数形参也为int
//1.指针数组
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 定义数组指针,指向包含4个int元素的一维数组,即指向二维数组的一行
int (*ptr)[4] = arr;
// 输出二维数组第一行第一个元素的值
printf("二维数组第一行第一个元素的值:%d\n", (*ptr)[0]);
//2.指针数组(函数)
返回类型 (*指针变量名)[数组长度](函数参数列表);
int (*funcArrayPtr[3])(int, int) = {add, subtract, multiply};
for (int i = 0; i < 3; i++) {
// 通过函数数组指针调用不同的函数
int result = (*funcArrayPtr[i])(num1, num2);
}
更多推荐



所有评论(0)