文章目录
C语言复习
extern
-
extern声明表明这是同文件夹别的
.c文件的a -
代码:
extern int a;extern int add(int, int); -
作用域规则:
- 文件作用域:默认全局可见
- 块作用域:在函数内声明时只在块内有效
-
注意:不能识别头文件,使用时避免重定义。
static extern无效(static优先)
析构顺序
析构顺序:局部变量:函数结束时立即析构(逆序);静态局部变量:程序结束时析构(按初始化逆序);全局变量/静态全局变量:程序结束时析构(同文件内逆序,跨文件未定义)
// 全局变量
class Global
{
public:
Global() { cout << "Global constructor" << endl; }
~Global() { cout << "Global destructor" << endl; }
};
Global global_var; // 全局变量
void test()
{
// 局部变量
class Local
{
public:
Local() { cout << "Local constructor" << endl; }
~Local() { cout << "Local destructor" << endl; }
};
Local local_var; // 局部变量
// 静态变量
class Static
{
public:
Static() { cout << "Static constructor" << endl; }
~Static() { cout << "Static destructor" << endl; }
};
{
static Static static_var; // 静态变量
}
static Static static_var2; // 静态变量
}
int main() {
cout << "In main function" << endl;
test(); // 调用函数,测试局部、静态和全局变量的析构顺序
return 0;
}
static
-
作用域:静态变量 (
static) 不能被其他文件通过extern使用,因为它们的作用域被限制在定义它的文件内。 -
访问:其他文件如果不包含声明静态变量的头文件,则无法访问该静态变量,即使它们尝试通过
extern进行声明。 -
析构顺序:局部变量:函数结束时立即析构(逆序);静态局部变量:程序结束时析构(按初始化逆序);全局变量/静态全局变量:程序结束时析构(同文件内逆序,跨文件未定义)
-
类内声明,类外定义:初始化列表和缺省值是对类内成员初始化,static之后,对象不是成为域内对象,并不会走初始化列表和缺省值。static成员变量应在类内声明,类外定义。
class A{ static size_t _t; }; // 这是声明 size_t A:_t = 1; // 这是定义 -
this指针:静态成员没有this指针,所以static修饰的函数,不能访问类内成员和方法,但能访问static的成员和方法。
-
在类A中定义static func();方法,类A中其他方法可以直接使用func()方法。
-
inline static和const static在类内声明,该如何处理?
-
实例化使用:静态方法和非静态方法在使用的时候有什么区别?——是否需要实例化才可以调用
例如,类A有
static void a();和非静态方法void b();调用
a(),可以A::a();直接调用,也可以先实例化,A m; m.a();调用;调用
b(),不能A::b();直接调用,需要先实例化,A m; m.b();调用;要注意的是,
using + 类名 + 方法都是非法的。
数组和指针
常量字符串
这里的“abcd”被存储在代码段,str1和str2指向的空间相同,所以打印出来地址结果相同。str3和str4则是通过赋值的方式,向开辟好的栈的空间中赋值。
const char *str1 = "abcd";
const char *str2 = "abcd";
char str3[5] = "abcd";
char str4[5] = "abcd";
cout << (void*)str3 << " " << (void*)str4 << '\n';
cout << (void*)str1 << " " << (void*)str2 << '\n';
数组指针
数组指针是指向整个数组的指针,而非指向数组首元素的指针。关键区别在于:——注意数组指针和二级指针的区别
-
数组指针的类型包含数组维度信息
-
对数组指针进行指针运算时,以整个数组为单位移动(这个部分要理解,数组在进行算数运算时会退化,这个退化就是退化成指针,退化成的这个指针什么类型的?退化成自己包含元素类型的地址——
int[][]类型数组的元素类型int(*)[],退化成int(*)[]运算、同理int[]数组退化成int*运算,然后对数组进行运算,移动字节)。#include <stdio.h> int main() { int matrix[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; /* int arr[3][4]; int **wrong = arr; // 错误:二级指针 != 数组指针 */ // 数组指针声明定义 int (*rowPtr)[4] = matrix; // 指向第一行 // 遍历二维数组 for(int i = 0; i < 3; i++) { printf("Row %d: ", i); for(int j = 0; j < 4; j++) { // 三种等价访问方式 printf("%2d ", rowPtr[i][j]); // 数组表示法 // printf("%2d ", *(*(rowPtr + i) + j)); // 指针表示法 // printf("%2d ", (*(rowPtr + i))[j]); // 混合表示法 } printf("\n"); } return 0; }
指针数组
数组元素均为指针类型。
char *strs[] = {"Apple", "Banana", "Cherry"};
printf("%s", strs[1]); // 输出"Banana"
函数指针
指向函数的指针,用于回调等场景。
int add(int a, int b) { return a + b; }
int (*func_ptr)(int, int) = add;
printf("%d", func_ptr(3, 5)); // 输出8
int add(int a, int b) { return a + b; }
void process(int a, int *call_back(int b, int c))
{
cout << (a + call_back(b, c));
}
// 要注意的是,和数组类似,函数在传参时传的也是指针,指针指向函数位置
// 使用时执行回调
process(2, add(2, 3));
const和指针
const int *p: 指向常量数据的指针(数据不可改)int *const p: 常量指针(地址不可改)
int a = 10, b = 20;
const int *p1 = &a; // *p1不可修改
int *const p2 = &a; // p2不可修改
// *p1 = 15; // 错误
p1 = &b; // 允许
// p2 = &b; // 错误
sizeof和指针和数组
-
指针大小固定(通常4或8字节)
-
对于指针,
sizeof返回的是指针本身占用的字节大小 -
sizeof(<数组名称>)返回数组总字节数,如果进行类似sizeof(<数组名称>+a)的运算,返回的是下一个维度的字节数。
-
代码如下:
// 测试代码 #include <stdio.h> // 数组退化成指针 void func(char arr[]) { printf("%zu", sizeof(arr)); // 输出(指针大小)!!!! } int main() { int matrix[3][4]; printf("整个数组大小: %zu\n", sizeof(matrix)); // 48 printf("单行大小: %zu\n", sizeof(matrix[0])); // 16 (4 int * 4) printf("元素大小: %zu\n", sizeof(matrix[0][0])); // 4 printf("行数: %zu\n", sizeof(matrix)/sizeof(matrix[0])); // 3 printf("列数: %zu\n", sizeof(matrix[0])/sizeof(matrix[0][0])); // 4 int (*num)[4] = matrix; cout << sizeof(matrix) << '\n'; // 48 cout << sizeof(matrix + 1) << '\n'; // 指针大小 cout << sizeof(*(matrix + 1)) << '\n'; // 16 cout << "-----------" << '\n'; cout << sizeof(num + 1) << '\n'; // 指针大小 cout << sizeof(*num); // 16 int arr[10]; int *p = arr; printf("%zu", sizeof(p)); // 输出8(64位系统)4则是32位系统 printf("%zu", sizeof(arr)); // 输出40(10*4) return 0; } -
动态分配的"数组"
int* dynArr = malloc(5 * sizeof(int)); printf("%zu", sizeof(dynArr)); // 输出8(指针大小)!
strlen和字符数组
strlen计算字符串长度(不含'\0'),sizeof包含结束符。
-
char str1[] = { 'a','b','c' };char str2[] = "abc";两者差异:对于“abc”字符串(char类型的数组),赋值等同于{‘a’,’b’,’c’,’\0’}(char strr[3] = "abc";就会发现数组溢出,也能证明这点。) -
strlen检测到‘\0’会停止++,立即返回统计的长度,printf %s也是相同机制,遇到‘\0’或才会停止。 -
返回类型
size_t可能的bug:——————size_t在进行运算的时候进行类型转换的优先级很高if (strlen("abc") - strlen("abcdef") < 0) // 3 - 6 ——》 3 + (-6) ——》 011 + 1000 0000 0000 0000 cout << "yes" << '\n'; else cout << "no" << '\n'; // 回显这个补码运算,运算视为加上一个负数,因为strlen返回size_t,运算结果巨大无比的正数。此处也要注意,int与size_t运算,会变成size_t之间的运算,int发生类型转换
-
对于str1,[]会与后文的内容相匹配,在栈上开辟相当的空间,因此不同于char str2[5] = { ‘a’,‘b’,‘c’ }(不完全初始化,未初始化的部分是0); 打印str2可以完美打印,而打印str1会打印出”烫烫“字样。strlen返回值,str2返回3,str1返回随机值(因为strlen会一直寻找到有\0’存在为止)
char str[] = "hello";
printf("%zu", strlen(str)); // 输出5
printf("%zu", sizeof(str)); // 输出6(包含'\0')
库函数的模拟实现
memcpy
内存复制(不处理重叠),n表示字节大小
void* my_memcpy(void* dst, const void* src, size_t n)
{
assert(dst && src);
char *d = dst; // 按字节拷贝!!!!!!用char
const char *s = src;
while (n--) *d++ = *s++;
return dst;
}
memmove
安全内存复制(处理重叠)
void* memmove(void* dest, const void* src, size_t n)
{
// 处理边界情况
if (dest == NULL || src == NULL || n == 0)
return dest; // 标准库返回dest,不是NULL
char* d = (char*)dest;
const char* s = (const char*)src;
// 检查内存重叠:dest在src之后且有重叠
if (d > s && d < s + n)
{
// 从后往前复制,避免覆盖
while (n--)
{
*(d + n) = *(s + n);
}
}
else
{
// 从前往后复制
while (n--)
{
*d++ = *s++;
}
}
return dest;
}
strstr
查找子串位置
char* my_strstr(const char* haystack, const char* needle)
{
if (!*needle) return (char*)haystack;
for (; *haystack; haystack++)
{
const char *h = haystack, *n = needle;
while (*h && *n && *h == *n) { h++; n++; }
if (!*n) return (char*)haystack;
}
return NULL;
}
strcpy/strlen/strcmp
字符串复制,要保证目的空间有足够空间,源字符串必须‘\0’结束,目标空间必须可修改
返回目标空间的起始地址,为了链式访问
char* my_strcpy(char* dest, const char* src)
{
assert(dest && src);
char *d = dest;
while ((*d++ = *src++));
return dest;
}
int main()
{
char arr1[] = "hello world";
char *p = "-----------------"; // cpp不允许,要加const属性,只有c能用
// strcpy(p, arr1); // 常量字符串不能修改!!!!!!
return 0;
}
字符串长度(三种方法)
size_t my_strlen(const char* s)
{
assert(s != NULL);
//size_t len = 0;
// 计数器
// while (*s++) len++;
//return len;
// 指针
char* ret = s;
while(*ret) ret++; // 注意ret应该停在'\0'位置
return ret - s;
}
// 递归
size_t my_strlen2(const char*s)
{
assert(s != NULL);
if(*s)
{
return 1 + my_strlen2(s + 1);
}
return 0;
}
字符串比较
int my_strcmp(const char* s1, const char* s2)
{
assert(s != NULL);
while (*s1 && *s1 == *s2) { s1++; s2++; }
return *(const unsigned char*)s1 - *(const unsigned char*)s2;
}
自定义类型
内存对齐
什么是内存对齐?
内存对齐是指数据在内存中的存放位置必须是某个数值的整数倍。这个数值称为"对齐边界"或"对齐要求"。
为什么需要内存对齐?
-
硬件要求:某些CPU架构要求特定类型的数据必须存储在特定的地址边界上
-
性能优化:对齐的内存访问通常比非对齐访问更快
-
原子操作:某些原子操作要求数据对齐
三条对齐规则:
-
第一个成员在与结构体偏移量为0的地址处;
-
其他成员变量要对其到自己对齐数的整数倍的地址处,对齐树=默认对齐数与该成员大小的较小值
-
结构体总大小为最大对齐数的整数倍。
对齐数的计算:
cout << "long对齐要求: " << alignof(long) << " 字节" << endl; // 8(Linux64) 或 4(Windows64)实例1:
struct Example1 { char a; // 偏移量0,满足1字节对齐 int b; // 偏移量4,满足4字节对齐(跳过1,2,3) char c; // 偏移量8,满足1字节对齐 }; // 结构体对齐要求 = max(1,4,1) = 4 // 内存布局:[a][填充3字节][b b b b][c][填充3字节] // 实际大小:9字节,但必须是4的倍数,所以填充到12字节实例2:
struct Inner { char a; int b; }; // Inner的对齐要求是4,大小是8 struct Outer { char x; // 偏移量0 Inner inner; // 偏移量4(必须是4的倍数) char y; // 偏移量12 }; // 总大小:16字节(必须是4的倍数)实例3:
#include <iostream> #include <cstddef> using namespace std; struct BasicAlign { char a; // 偏移量0, 大小1 short b; // 偏移量2, 大小2 (跳过偏移量1) int c; // 偏移量4, 大小4 char d; // 偏移量8, 大小1 }; int main() { cout << "BasicAlign大小: " << sizeof(BasicAlign) << endl; // 12 cout << "BasicAlign对齐: " << alignof(BasicAlign) << endl; // 4 BasicAlign obj; cout << "a的偏移量: " << offsetof(BasicAlign, a) << endl; // 0 cout << "b的偏移量: " << offsetof(BasicAlign, b) << endl; // 2 cout << "c的偏移量: " << offsetof(BasicAlign, c) << endl; // 4 cout << "d的偏移量: " << offsetof(BasicAlign, d) << endl; // 8 } // 偏移量: 0 1 2 3 4 5 6 7 8 9 10 11 // 内容: [a][P][b b][c c c c][d][P][P ][P ]实例4:
struct ComplexAlign { char a; // 偏移量0 double b; // 偏移量8 (跳过1-7) char c; // 偏移量16 int d; // 偏移量20 (跳过17-19) char e[3]; // 偏移量24 }; int main() { cout << "ComplexAlign大小: " << sizeof(ComplexAlign) << endl; // 32 cout << "ComplexAlign对齐: " << alignof(ComplexAlign) << endl; // 8 ComplexAlign obj; cout << "a偏移量: " << offsetof(ComplexAlign, a) << endl; // 0 cout << "b偏移量: " << offsetof(ComplexAlign, b) << endl; // 8 cout << "c偏移量: " << offsetof(ComplexAlign, c) << endl; // 16 cout << "d偏移量: " << offsetof(ComplexAlign, d) << endl; // 20 cout << "e偏移量: " << offsetof(ComplexAlign, e) << endl; // 24 } // 偏移量: 0-7 8-15 16-19 20-23 24-31 // 内容: [a+7P][b b b b b b b b][c+3P][d d d d][e e e+5P]实例5:
struct ArrayAlign { char a; int arr[3]; // 数组按元素类型对齐 char b; }; // a: 偏移量0 // arr: 偏移量4 (int需要4字节对齐) // b: 偏移量16 // 总大小: 20字节实例6:
struct Inner1 { char a; int b; }; // 大小8,对齐4 struct Inner2 { char x; double y; }; // 大小16,对齐8 struct Nested { char start; // 偏移量0 Inner1 i1; // 偏移量4 (按4对齐) char middle; // 偏移量12 Inner2 i2; // 偏移量16 (按8对齐) char end; // 偏移量32 }; // 总对齐要求:max(1,4,1,8,1) = 8 // 总大小:40字节 (33+7填充)实例7(结构体实现位域):
struct BitField { unsigned int a : 3; // 3位 unsigned int b : 5; // 5位 unsigned int c : 8; // 8位,总共16位,在一个int内 unsigned int d : 20; // 20位,需要新的int char e; // 1字节 }; // 大小:12字节 (两个int + 一个char + 填充)
修改对齐方式方法:
#pragma pack(1) // 设置1字节对齐
struct Packed {
char a;
int b;
char c;
};
#pragma pack() // 恢复默认对齐
// Packed大小:6字节 (无填充)
联合
union UnionAlign {
char a;
int b;
double c;
};
// 大小:8字节 (最大成员double的大小)
// 对齐:8字节 (最大成员double的对齐要求)
枚举
可以指定枚举值,枚举值默认是上一个值加一,第一个枚举值如果不指定值,默认是0.
相比宏,枚举可读性高、可以定义在局部、又类型检查,比宏更安全。
// 基本枚举定义
enum Color {
RED, // 默认值为0
GREEN, // 默认值为1
BLUE // 默认值为2
};
// 使用枚举
enum Color myColor = RED;
数据存储
本人博客指路:数据存储详解-优快云博客
主要内容包括:
- 整形存储规则:原反补;
- 大小端;
- 浮点数的存储;
- 整型提升和截断
编译链接
*.c文件通过编译链接才会形成*.exe可执行文件,这部分粗略讲解编译链接各部分的工作。详细步骤在《编译原理》这本书会有讲解。在使用vs中,环境会自动给你编译链接,并且运行可执行文件。所以本文讲解各部分都使用Linux gcc编译器下进行,目的在于理解各个部分的作用。
编译
编译是把
*.c文件形成*.o目标文件的过程,编译分为三步骤:预处理、编译、汇编。(这俩编译不一样~)
预处理
- Linux指令:
gcc -E test.c -o test.i - 作用:将
*.c文件预处理,形成*.i - 预处理内容:
- 删除
#define,对宏进行宏替换:如果宏函数中的参数带有宏,先进行参数的替换,在进行函数的替换; - 删除
#include,对头文件展开:将此文件包含的头文件中的内容直接ctrl c + ctrl v到你的文件里; - 对条件编译进行处理;
- 保留
#pragma once,当一个文件被包含,如果有#pragma once,就不会再展开了,这样可以防止头文件被重复包含; - 删除注释:把你注释删了。
- 删除
- 注意,我们可以利用预处理的原理做一些操作去优化代码:
#pragma once或者使用条件编译,保证头文件只展开一次,简化*.i的文件内容,那么以后你的代码形成*.exe文件内容也会减少,加载到内存所占空间也会减少。
编译
- Linux指令:
gcc -S test.i -o test.s - 作用:将
*.i文件编译,形成*.s - 编译内容:
- 语义分析器进行语义分析,分析语法是否发生错误(vs,vscode等环境会在你写代码的时候也进行检查)
- 他是怎么完成语义分析的呢——以操作符作为父节点,操作数作为子节点,生成语法树,检验是否有语法错误
- 把c代码形成汇编代码,为下一步操作做准备
- 语义分析器进行语义分析,分析语法是否发生错误(vs,vscode等环境会在你写代码的时候也进行检查)
汇编
- Linux指令:
gcc -c test.s -o test.o - 作用:将
*.s文件预处理,形成*.o - 汇编内容:
- 根据函数生成符号表,为链接时合并符号表的操作做准备(就是对函数地址重定位的过程)
- 把汇编代码形成机器码
链接
- Linux指令:
gcc test1.o test2.o test3.o -o test.exe - 作用:将
test1.o test2.o test3.o文件链接,形成*.exe - 链接内容:
- 符号表合并,对函数地址进行重定位(如果头文件包含函数,并且调用了这个函数,但是函数未定义,此时就会发生链接错误,因为你找不到这个函数的地址)
宏
-
作用域:全局。即使在局部定义,作用域仍为全局
-
定义和使用宏如下:
#ifdef DEBUG // 条件编译 printf("Debugging...\n"); #define MAX 100 // 定义表示常量 #define ADD(x,y) ((x) * (y)) // 定义一个乘法的宏函数 #define CONCAT(x, y) x ## y // 实现字符粘合 #define PRINT(format, v) printf("the value of" " #v " "is "format, v); // 通过#实现字符串化 // 实现一个日志宏 // \用来续行 // 最后一行不用加\ #define LOG(level, format, ...) \ { \ if (level > LDBG) \ { \ time_t t = time(nullptr); \ struct tm *lt = localtime(&t); \ char time_tmp[32] = {0}; \ strftime(time_tmp, 31, "%m-%d %T", lt); \ fprintf(stdout, "[%s][%s:%d]", format "\n",time_tmp, __FILE__, __LINE__ ,##__VA_ARGS__);\ // 这里__FILE__, __LINE__ 就是宏,__DATE__ __TIME__ 也是宏,用于查看日期,__STDC__可以查看编译器是否完全按照C标准 }\ } #include <stdio.h> // 实现字符粘合的使用 int main() { int ab = 10; printf("%d\n", CONCAT(a, b)); // 输出 10 return 0; } #endif // 条件编译结束 -
宏在预处理的时候由编译器在使用宏的位置进行替换。
- 使用宏定义标志常量,方便进行大范围修改工程的参数达到目的。
- 宏函数:
- 优点:不用通过寻找函数地址调用函数、不用建立栈帧,速度比一般函数会快。另外,宏函数不用指定类型参数。
- 缺点:宏函数不允许递归,而且不能调试,因为不能指定类型参数,不严谨,宏函数替换会直接成为代码段的一部分。而且因为在使用宏的位置直接进行替换,所以会加大代码段长度,
- 这里有个折中的方法,就是cpp引入的inline,但是inline不一定会产生想要的效果,这里不过多解释了。
-
*
#define ADD(x,y) ((x) * (y))*详解- 宏替换是文本替换(纯粹的文本替换)
- *
#define ADD(x,y) (x * y)*可能的问题:cout << ADD(1 + 1, 2 + 2)出5,这是因为进行文本替换后,进行计算(1+1*2+2)
#define ADD(x,y) (x) * (y)可能出现问题原因同上
-
补充:
- offsetof(struct, 成员)宏可以查看该成员在结构体的偏移量,头文件
<stddef.h> - 命令行定义:
gcc -D sz=10 test.c可以指定宏sz的值(在编译的时候指定参数是多少)
- offsetof(struct, 成员)宏可以查看该成员在结构体的偏移量,头文件
缓冲区
char passwd[32] = { 0 };
// 输入passwd
scanf("%s", passwd);
// 输入是否确认
char ret = getchar();
if("Y" == ret)
printf("yes");
else
printf("no");
scanf从缓冲区拿去数据知道空格和\n,空格和\n并不会拿取,如果此时通过getchar拿取数据,并不会拿到刚刚输入的“Y”,而是直接拿\n,所以一定会到no语句。
while((ch = getchar() != '\n')){}; // 去除\n和之前的东西
136

被折叠的 条评论
为什么被折叠?



