C语言复习

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;
}

自定义类型

内存对齐

什么是内存对齐?

内存对齐是指数据在内存中的存放位置必须是某个数值的整数倍。这个数值称为"对齐边界"或"对齐要求"。

为什么需要内存对齐?

  1. 硬件要求:某些CPU架构要求特定类型的数据必须存储在特定的地址边界上

  2. 性能优化:对齐的内存访问通常比非对齐访问更快

  3. 原子操作:某些原子操作要求数据对齐

三条对齐规则:

  • 第一个成员在与结构体偏移量为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代码形成汇编代码,为下一步操作做准备
汇编
  • 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的值(在编译的时候指定参数是多少)

缓冲区

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和之前的东西
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值