C/C++核心知识点详解

发布于:2025-07-28 ⋅ 阅读:(17) ⋅ 点赞:(0)

C/C++核心知识点详解

1. 变量的声明与定义:内存分配的本质区别

核心概念

在C/C++中,变量的声明定义是两个完全不同的概念:

  • 声明(Declaration):告诉编译器变量的名称和类型,但不分配内存空间
  • 定义(Definition):不仅声明变量,还为其分配实际的内存空间

实际应用示例

// 变量声明:使用extern关键字,不分配内存
extern int global_var;  // 声明一个整型变量,告诉编译器这个变量在别处定义

// 变量定义:分配内存空间
int global_var = 100;   // 定义变量并分配内存,可以初始化

// 函数声明
int add(int a, int b);  // 函数声明,不包含函数体

// 函数定义
int add(int a, int b) { // 函数定义,包含完整实现
    return a + b;
}

重要规则

  • 一个变量可以在多个地方声明,但只能在一个地方定义
  • 声明可以重复,定义不能重复
  • 使用变量前必须先声明,使用时才需要定义

2. 不同数据类型与零值比较的标准写法

为什么要规范比较写法?

不同数据类型与零值比较时,写法不当可能导致逻辑错误或程序崩溃。

标准比较方式

bool类型:直接判断真假
bool is_valid = true;

// 正确写法:直接使用布尔值
if (is_valid) {
    // 条件为真时执行
    printf("数据有效\n");
} else {
    // 条件为假时执行
    printf("数据无效\n");
}
int类型:与0比较
int count = 10;

// 正确写法:将常量放在左边(防御性编程)
if (0 != count) {
    // count不等于0时执行
    printf("计数值为:%d\n", count);
} else {
    // count等于0时执行
    printf("计数为零\n");
}
指针类型:与NULL比较
int* ptr = nullptr;

// 正确写法:将NULL放在左边
if (NULL == ptr) {
    // 指针为空时执行
    printf("指针为空\n");
} else {
    // 指针不为空时执行
    printf("指针指向的值:%d\n", *ptr);
}
float类型:使用精度范围比较
float value = 0.0001f;
const float EPSILON = 1e-6f;  // 定义精度阈值

// 正确写法:判断是否在精度范围内
if ((value >= -EPSILON) && (value <= EPSILON)) {
    // 认为等于零
    printf("浮点数接近零\n");
} else {
    // 不等于零
    printf("浮点数值:%f\n", value);
}

防御性编程技巧

将常量放在比较运算符左边的好处:

// 错误示例:容易写错
if (count = 0) {  // 误将==写成=,编译通过但逻辑错误
    // 永远不会执行
}

// 正确示例:编译器会报错
if (0 = count) {  // 编译错误,无法给常量赋值
    // 编译器直接报错,避免逻辑错误
}

3. sizeof与strlen:编译时计算 vs 运行时计算

本质区别分析

sizeof:编译时操作符
// sizeof是操作符,不是函数
int arr[10];
char str[] = "Hello";

// 编译时就确定结果
size_t arr_size = sizeof(arr);      // 结果:40字节(10个int)
size_t str_size = sizeof(str);      // 结果:6字节(包含'\0')
size_t int_size = sizeof(int);      // 结果:4字节(平台相关)
strlen:运行时库函数
#include <string.h>

char str[] = "Hello";
char* ptr = "World";

// 运行时计算字符串长度
size_t len1 = strlen(str);  // 结果:5(不包含'\0')
size_t len2 = strlen(ptr);  // 结果:5(不包含'\0')

数组退化现象

void test_array(char arr[]) {
    // 数组作为参数时退化为指针
    printf("sizeof(arr) = %zu\n", sizeof(arr));    // 输出指针大小(8字节)
    printf("strlen(arr) = %zu\n", strlen(arr));    // 输出字符串长度
}

int main() {
    char str[] = "Hello";
    printf("sizeof(str) = %zu\n", sizeof(str));    // 输出:6
    printf("strlen(str) = %zu\n", strlen(str));    // 输出:5
    
    test_array(str);  // 数组退化为指针
    return 0;
}

性能对比

  • sizeof:编译时确定,零运行时开销
  • strlen:需要遍历字符串,时间复杂度O(n)

4. static关键字:C语言 vs C++的功能扩展

C语言中的static

// 1. 局部静态变量:函数调用间保持值
void counter() {
    static int count = 0;  // 只初始化一次
    count++;
    printf("调用次数:%d\n", count);
}

// 2. 全局静态变量:限制作用域在当前文件
static int file_global = 100;  // 只在当前文件可见

// 3. 静态函数:限制函数作用域在当前文件
static void helper_function() {
    printf("这是一个静态函数\n");
}

C++中的static扩展功能

class MyClass {
private:
    static int class_count;     // 静态成员变量:所有对象共享
    int instance_id;            // 实例成员变量:每个对象独有

public:
    MyClass() {
        instance_id = ++class_count;  // 每创建一个对象,计数加1
    }
    
    // 静态成员函数:不依赖具体对象实例
    static int get_count() {
        return class_count;     // 只能访问静态成员
        // return instance_id;  // 错误:无法访问非静态成员
    }
};

// 静态成员变量必须在类外定义
int MyClass::class_count = 0;

// 使用示例
int main() {
    MyClass obj1, obj2, obj3;
    printf("创建的对象数量:%d\n", MyClass::get_count());  // 输出:3
    return 0;
}

静态变量的内存特点

  • 存储在静态存储区,程序结束时才销毁
  • 只初始化一次,后续调用保持上次的值
  • 可以在不同函数调用间传递信息

5. malloc vs new:C风格 vs C++风格的内存管理

基本区别对比

malloc/free:C语言风格
#include <stdlib.h>

// 分配内存
int* ptr = (int*)malloc(sizeof(int) * 10);  // 分配10个int的空间
if (ptr == NULL) {
    printf("内存分配失败\n");
    return -1;
}

// 使用内存
for (int i = 0; i < 10; i++) {
    ptr[i] = i * i;  // 需要手动初始化
}

// 释放内存
free(ptr);      // 只释放内存,不调用析构函数
ptr = NULL;     // 防止悬空指针
new/delete:C++风格
// 分配单个对象
int* single_ptr = new int(42);          // 分配并初始化
delete single_ptr;                      // 释放单个对象

// 分配数组
int* array_ptr = new int[10];           // 分配数组
delete[] array_ptr;                     // 释放数组(注意使用delete[])

// 分配类对象
class Person {
public:
    Person(const char* name) {
        printf("构造函数:创建 %s\n", name);
    }
    ~Person() {
        printf("析构函数:销毁对象\n");
    }
};

Person* person = new Person("张三");     // 自动调用构造函数
delete person;                          // 自动调用析构函数

构造函数与析构函数的区别

class TestClass {
public:
    TestClass() { 
        printf("对象被构造\n"); 
        data = new int[100];  // 分配资源
    }
    ~TestClass() { 
        printf("对象被析构\n"); 
        delete[] data;        // 释放资源
    }
private:
    int* data;
};

// malloc方式:不会调用构造/析构函数
TestClass* obj1 = (TestClass*)malloc(sizeof(TestClass));
// 没有输出"对象被构造"
free(obj1);  // 没有输出"对象被析构",可能导致内存泄漏

// new方式:自动调用构造/析构函数
TestClass* obj2 = new TestClass();  // 输出"对象被构造"
delete obj2;                        // 输出"对象被析构"

混用的危险性

// 错误示例:不要混用
int* ptr1 = (int*)malloc(sizeof(int));
delete ptr1;    // 错误:malloc的内存不能用delete释放

int* ptr2 = new int;
free(ptr2);     // 错误:new的内存不能用free释放

6. 宏定义的陷阱:副作用问题

标准MIN宏的实现

#define MIN(a, b) ((a) <= (b) ? (a) : (b))

// 基本使用
int x = 5, y = 3;
int min_val = MIN(x, y);  // 结果:3

宏的副作用问题

#define MIN(a, b) ((a) <= (b) ? (a) : (b))

int main() {
    int x = 5;
    int* p = &x;
    
    // 危险的调用方式
    int result = MIN(++(*p), 10);
    
    // 宏展开后变成:
    // int result = ((++(*p)) <= (10) ? (++(*p)) : (10));
    // ++(*p)被执行了两次!
    
    printf("x = %d\n", x);      // x可能是7而不是6
    printf("result = %d\n", result);
    
    return 0;
}

更安全的实现方式

// 使用内联函数替代宏(C++推荐)
inline int min_safe(int a, int b) {
    return (a <= b) ? a : b;
}

// 或者使用临时变量的宏(C语言)
#define MIN_SAFE(a, b) ({ \
    typeof(a) _a = (a); \
    typeof(b) _b = (b); \
    (_a <= _b) ? _a : _b; \
})

7. volatile关键字:处理不可预测的变量变化

volatile的作用机制

volatile告诉编译器:这个变量可能被程序外部因素修改,不要进行优化。

典型应用场景

中断服务程序
volatile int interrupt_flag = 0;  // 中断标志

// 中断服务函数
void interrupt_handler() {
    interrupt_flag = 1;  // 中断发生时设置标志
}

// 主程序
int main() {
    while (interrupt_flag == 0) {
        // 等待中断发生
        // 如果没有volatile,编译器可能优化成死循环
    }
    printf("中断已处理\n");
    return 0;
}
硬件寄存器访问
// 硬件寄存器地址
volatile unsigned int* const HARDWARE_REG = (unsigned int*)0x40000000;

void read_sensor() {
    unsigned int value = *HARDWARE_REG;  // 每次都从硬件读取
    printf("传感器值:%u\n", value);
}
多线程共享变量
volatile bool thread_running = true;

void worker_thread() {
    while (thread_running) {  // 确保每次都检查最新值
        // 执行工作
        printf("线程运行中...\n");
        sleep(1);
    }
    printf("线程退出\n");
}

void stop_thread() {
    thread_running = false;  // 通知线程停止
}

volatile指针的不同含义

int value = 100;

// 指向volatile变量的指针
volatile int* ptr1 = &value;        // 指向的内容是volatile的
*ptr1 = 200;                        // 每次写入都不会被优化

// volatile指针指向普通变量
int* volatile ptr2 = &value;        // 指针本身是volatile的
ptr2 = &another_value;              // 指针的修改不会被优化

// volatile指针指向volatile变量
volatile int* volatile ptr3 = &value; // 指针和内容都是volatile的

8. 数组名与数组地址:a vs &a的本质区别

概念解析

  • a:数组名,表示数组首元素的地址
  • &a:数组的地址,表示整个数组的地址

代码分析实例

#include <stdio.h>

int main() {
    int a[5] = {1, 2, 3, 4, 5};
    
    printf("a = %p\n", a);          // 数组首元素地址
    printf("&a = %p\n", &a);        // 整个数组的地址
    printf("a+1 = %p\n", a+1);      // 下一个元素地址(+4字节)
    printf("&a+1 = %p\n", &a+1);    // 下一个数组地址(+20字节)
    
    // 关键代码分析
    int* ptr = (int*)(&a + 1);      // 指向数组后面的位置
    printf("*(a+1) = %d\n", *(a+1));    // 输出:2(第二个元素)
    printf("*(ptr-1) = %d\n", *(ptr-1)); // 输出:5(最后一个元素)
    
    return 0;
}

内存布局图解

内存地址:  1000   1004   1008   1012   1016   1020
数组内容:  [ 1 ]  [ 2 ]  [ 3 ]  [ 4 ]  [ 5 ]
          ↑                              ↑      ↑
          a                              |      &a+1
          &a                             |
                                    a+4或&a[4]

指针运算的区别

int a[5] = {1, 2, 3, 4, 5};

// a+1:移动一个int的大小(4字节)
int* p1 = a + 1;        // 指向a[1]

// &a+1:移动整个数组的大小(20字节)
int* p2 = (int*)(&a + 1); // 指向数组后面的位置

// 验证
printf("p1指向的值:%d\n", *p1);      // 输出:2
printf("p2-1指向的值:%d\n", *(p2-1)); // 输出:5

9. C/C++程序内存布局:五大存储区域详解

内存分区概述

C/C++程序运行时,内存被划分为5个主要区域,每个区域有不同的特点和用途。

1. 程序代码区(Text Segment)

// 存储编译后的机器代码
void function1() {
    printf("这个函数的代码存储在代码区\n");
}

int main() {
    printf("main函数的代码也在代码区\n");
    return 0;
}

特点:只读、共享、程序加载时确定大小

2. 全局/静态存储区(Data Segment)

// 已初始化的全局变量
int global_var = 100;           // 存储在已初始化数据区

// 未初始化的全局变量
int uninitialized_global;       // 存储在BSS区(自动初始化为0)

// 静态变量
static int static_var = 200;    // 存储在已初始化数据区

void function() {
    static int local_static;    // 存储在BSS区
}

特点:程序运行期间一直存在、自动初始化为0(BSS区)

3. 栈区(Stack)

void stack_demo() {
    int local_var = 10;         // 局部变量,存储在栈上
    char buffer[1024];          // 局部数组,存储在栈上
    
    printf("local_var地址:%p\n", &local_var);
    printf("buffer地址:%p\n", buffer);
    
    // 函数结束时,这些变量自动销毁
}

void recursive_function(int n) {
    int local = n;              // 每次递归调用都在栈上分配
    if (n > 0) {
        recursive_function(n - 1);
    }
}

特点

  • 自动管理(函数结束自动释放)
  • 访问速度快
  • 大小有限(通常几MB)
  • 后进先出(LIFO)

4. 堆区(Heap)

void heap_demo() {
    // 动态分配内存
    int* heap_ptr = (int*)malloc(sizeof(int) * 100);
    if (heap_ptr == NULL) {
        printf("内存分配失败\n");
        return;
    }
    
    // 使用堆内存
    for (int i = 0; i < 100; i++) {
        heap_ptr[i] = i;
    }
    
    // 必须手动释放
    free(heap_ptr);
    heap_ptr = NULL;  // 防止悬空指针
}

void cpp_heap_demo() {
    // C++风格的堆内存管理
    int* ptr = new int[100];    // 分配
    // 使用内存...
    delete[] ptr;               // 释放
}

特点

  • 手动管理(程序员负责分配和释放)
  • 大小灵活
  • 访问速度相对较慢
  • 容易产生内存泄漏和碎片

5. 文字常量区(String Literal Pool)

void string_demo() {
    char* str1 = "Hello World";     // 字符串存储在常量区
    char* str2 = "Hello World";     // 可能与str1指向同一地址
    
    char arr[] = "Hello World";     // 字符串复制到栈上
    
    printf("str1地址:%p\n", str1);
    printf("str2地址:%p\n", str2);
    printf("arr地址:%p\n", arr);
    
    // str1[0] = 'h';  // 错误:不能修改常量区内容
    arr[0] = 'h';      // 正确:可以修改栈上的副本
}

内存布局示意图

高地址
┌─────────────────┐
│     栈区        │ ← 向下增长
│   (局部变量)     │
├─────────────────┤
│       ↓         │
│                 │
│       ↑         │
├─────────────────┤
│     堆区        │ ← 向上增长
│   (动态分配)     │
├─────────────────┤
│   未初始化数据   │
│    (BSS段)      │
├─────────────────┤
│   已初始化数据   │
│   (Data段)      │
├─────────────────┤
│   文字常量区     │
├─────────────────┤
│    程序代码区    │
└─────────────────┘
低地址

10. 字符串操作函数对比:strcpy、sprintf、memcpy

功能定位分析

strcpy:字符串到字符串的复制
#include <string.h>

void strcpy_demo() {
    char source[] = "Hello World";
    char destination[20];
    
    // 复制字符串(包括结尾的'\0')
    strcpy(destination, source);
    printf("复制结果:%s\n", destination);
    
    // 注意:不检查目标缓冲区大小,可能溢出
    // 更安全的版本:strncpy
    strncpy(destination, source, sizeof(destination) - 1);
    destination[sizeof(destination) - 1] = '\0';  // 确保以'\0'结尾
}
sprintf:格式化输出到字符串
#include <stdio.h>

void sprintf_demo() {
    char buffer[100];
    int age = 25;
    float height = 175.5f;
    char name[] = "张三";
    
    // 将多种数据类型格式化为字符串
    sprintf(buffer, "姓名:%s,年龄:%d,身高:%.1f厘米", 
            name, age, height);
    
    printf("格式化结果:%s\n", buffer);
    
    // 更安全的版本:snprintf
    snprintf(buffer, sizeof(buffer), "安全的格式化:%s", name);
}
memcpy:内存块到内存块的复制
#include <string.h>

void memcpy_demo() {
    // 复制整数数组
    int source[] = {1, 2, 3, 4, 5};
    int destination[5];
    
    memcpy(destination, source, sizeof(source));
    
    // 复制结构体
    struct Person {
        char name[20];
        int age;
    };
    
    struct Person p1 = {"李四", 30};
    struct Person p2;
    
    memcpy(&p2, &p1, sizeof(struct Person));
    printf("复制的结构体:%s, %d\n", p2.name, p2.age);
    
    // 复制部分内存
    char str1[] = "Hello World";
    char str2[20];
    memcpy(str2, str1, 5);  // 只复制前5个字符
    str2[5] = '\0';         // 手动添加结束符
    printf("部分复制:%s\n", str2);  // 输出:Hello
}

性能对比测试

#include <time.h>

void performance_test() {
    const int TEST_SIZE = 1000000;
    char source[1000];
    char destination[1000];
    clock_t start, end;
    
    // 初始化源数据
    memset(source, 'A', sizeof(source) - 1);
    source[sizeof(source) - 1] = '\0';
    
    // 测试memcpy性能
    start = clock();
    for (int i = 0; i < TEST_SIZE; i++) {
        memcpy(destination, source, sizeof(source));
    }
    end = clock();
    printf("memcpy耗时:%f秒\n", (double)(end - start) / CLOCKS_PER_SEC);
    
    // 测试strcpy性能
    start = clock();
    for (int i = 0; i < TEST_SIZE; i++) {
        strcpy(destination, source);
    }
    end = clock();
    printf("strcpy耗时:%f秒\n", (double)(end - start) / CLOCKS_PER_SEC);
    
    // 测试sprintf性能
    start = clock();
    for (int i = 0; i < TEST_SIZE; i++) {
        sprintf(destination, "%s", source);
    }
    end = clock();
    printf("sprintf耗时:%f秒\n", (double)(end - start) / CLOCKS_PER_SEC);
}

使用场景选择指南

  • strcpy:纯字符串复制,需要自动处理’\0’结尾
  • sprintf:需要格式化多种数据类型为字符串
  • memcpy:原始内存复制,最高效,适合大块数据

11. 直接内存操作:指定地址赋值技术

基本概念

在嵌入式开发或系统编程中,经常需要直接操作特定内存地址的数据。

实现方法

void memory_operation_demo() {
    // 将整数值0xaa66写入地址0x67a9
    int* ptr;                    // 声明整型指针
    ptr = (int*)0x67a9;         // 将地址强制转换为整型指针
    *ptr = 0xaa66;              // 向该地址写入数据
    
    // 读取验证
    printf("地址0x67a9的值:0x%x\n", *ptr);
}

实际应用场景

硬件寄存器操作
// 定义硬件寄存器地址
#define GPIO_BASE_ADDR    0x40020000
#define GPIO_OUTPUT_REG   (GPIO_BASE_ADDR + 0x14)
#define GPIO_INPUT_REG    (GPIO_BASE_ADDR + 0x10)

void gpio_control() {
    // 控制GPIO输出
    volatile unsigned int* gpio_output = (volatile unsigned int*)GPIO_OUTPUT_REG;
    *gpio_output = 0xFF;  // 设置所有引脚为高电平
    
    // 读取GPIO输入
    volatile unsigned int* gpio_input = (volatile unsigned int*)GPIO_INPUT_REG;
    unsigned int input_value = *gpio_input;
    printf("GPIO输入值:0x%x\n", input_value);
}
内存映射文件操作
#include <sys/mman.h>
#include <fcntl.h>

void memory_mapped_file() {
    int fd = open("data.bin", O_RDWR);
    if (fd == -1) return;
    
    // 将文件映射到内存
    void* mapped_addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, 
                            MAP_SHARED, fd, 0);
    
    if (mapped_addr != MAP_FAILED) {
        // 直接操作内存就是操作文件
        int* data_ptr = (int*)mapped_addr;
        *data_ptr = 0x12345678;  // 写入数据到文件
        
        // 解除映射
        munmap(mapped_addr, 1024);
    }
    
    close(fd);
}

安全注意事项

void safe_memory_access() {
    // 1. 检查地址有效性
    void* addr = (void*)0x67a9;
    if (addr == NULL) {
        printf("无效地址\n");
        return;
    }
    
    // 2. 使用volatile防止编译器优化
    volatile int* ptr = (volatile int*)addr;
    
    // 3. 异常处理(在支持的系统上)
    try {
        *ptr = 0xaa66;
    } catch (...) {
        printf("内存访问异常\n");
    }
}

12. 面向对象三大特征深度解析

1. 封装性(Encapsulation):数据隐藏与接口设计

基本概念

封装是将数据和操作数据的方法组合在一起,通过访问控制来隐藏内部实现细节。

class BankAccount {
private:
    double balance;          // 私有数据:外部无法直接访问
    string account_number;   // 私有数据:账户安全信息
    
public:
    // 公有接口:提供安全的访问方式
    BankAccount(string acc_num, double initial_balance) {
        account_number = acc_num;
        balance = initial_balance;
    }
    
    // 存款操作:控制数据修改方式
    bool deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            return true;
        }
        return false;  // 拒绝无效操作
    }
    
    // 取款操作:包含业务逻辑验证
    bool withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;  // 余额不足或金额无效
    }
    
    // 查询余额:只读访问
    double get_balance() const {
        return balance;
    }
};
访问控制级别
class AccessDemo {
private:
    int private_data;        // 只有类内部可以访问
    
protected:
    int protected_data;      // 类内部和子类可以访问
    
public:
    int public_data;         // 任何地方都可以访问
    
    void demo_access() {
        private_data = 1;    // 正确:类内部访问
        protected_data = 2;  // 正确:类内部访问
        public_data = 3;     // 正确:类内部访问
    }
};

class DerivedClass : public AccessDemo {
public:
    void test_access() {
        // private_data = 1;     // 错误:无法访问私有成员
        protected_data = 2;      // 正确:子类可以访问保护成员
        public_data = 3;         // 正确:公有成员任何地方可访问
    }
};

2. 继承性(Inheritance):代码复用与层次结构

基本继承概念
// 基类:动物
class Animal {
protected:
    string name;
    int age;
    
public:
    Animal(string n, int a) : name(n), age(a) {}
    
    // 虚函数:允许子类重写
    virtual void make_sound() {
        cout << name << "发出声音" << endl;
    }
    
    // 普通成员函数
    void eat() {
        cout << name << "正在吃东西" << endl;
    }
    
    virtual ~Animal() {}  // 虚析构函数
};

// 派生类:狗
class Dog : public Animal {
private:
    string breed;  // 狗特有的属性
    
public:
    Dog(string n, int a, string b) : Animal(n, a), breed(b) {}
    
    // 重写基类的虚函数
    virtual void make_sound() override {
        cout << name << "汪汪叫" << endl;
    }
    
    // 狗特有的行为
    void wag_tail() {
        cout << name << "摇尾巴" << endl;
    }
};

// 派生类:猫
class Cat : public Animal {
public:
    Cat(string n, int a) : Animal(n, a) {}
    
    virtual void make_sound() override {
        cout << name << "喵喵叫" << endl;
    }
    
    void climb_tree() {
        cout << name << "爬树" << endl;
    }
};
继承的三种方式
class Base {
public:    int pub_member;
protected: int prot_member;
private:   int priv_member;
};

// 公有继承:保持访问级别
class PublicDerived : public Base {
    // pub_member  -> public
    // prot_member -> protected
    // priv_member -> 不可访问
};

// 保护继承:公有成员变为保护
class ProtectedDerived : protected Base {
    // pub_member  -> protected
    // prot_member -> protected
    // priv_member -> 不可访问
};

// 私有继承:所有成员变为私有
class PrivateDerived : private Base {
    // pub_member  -> private
    // prot_member -> private
    // priv_member -> 不可访问
};

3. 多态性(Polymorphism):一个接口多种实现

运行时多态(动态多态)
void demonstrate_polymorphism() {
    // 创建不同类型的动物对象
    Animal* animals[] = {
        new Dog("旺财", 3, "金毛"),
        new Cat("咪咪", 2),
        new Dog("小黑", 5, "土狗")
    };
    
    // 多态调用:同一接口,不同实现
    for (int i = 0; i < 3; i++) {
        animals[i]->make_sound();  // 根据实际对象类型调用相应函数
        animals[i]->eat();         // 调用基类函数
    }
    
    // 清理内存
    for (int i = 0; i < 3; i++) {
        delete animals[i];
    }
}
编译时多态(静态多态)
class Calculator {
public:
    // 函数重载:同名函数,不同参数
    int add(int a, int b) {
        return a + b;
    }
    
    double add(double a, double b) {
        return a + b;
    }
    
    int add(int a, int b, int c) {
        return a + b + c;
    }
};

// 模板实现编译时多态
template<typename T>
T generic_add(T a, T b) {
    return a + b;
}

void polymorphism_demo() {
    Calculator calc;
    
    // 编译器根据参数类型选择合适的函数
    cout << calc.add(1, 2) << endl;        // 调用int版本
    cout << calc.add(1.5, 2.5) << endl;    // 调用double版本
    cout << calc.add(1, 2, 3) << endl;     // 调用三参数版本
    
    // 模板多态
    cout << generic_add(10, 20) << endl;      // int版本
    cout << generic_add(1.1, 2.2) << endl;   // double版本
}

13. C++空类的隐式成员函数

编译器自动生成的六个函数

当定义一个空类时,编译器会自动生成以下成员函数:

class EmptyClass {
    // 编译器自动生成以下函数:
    
    // 1. 默认构造函数
    // EmptyClass() {}
    
    // 2. 拷贝构造函数
    // EmptyClass(const EmptyClass& other) {}
    
    // 3. 析构函数
    // ~EmptyClass() {}
    
    // 4. 赋值运算符
    // EmptyClass& operator=(const EmptyClass& other) { return *this; }
    
    // 5. 取址运算符
    // EmptyClass* operator&() { return this; }
    
    // 6. const取址运算符
    // const EmptyClass* operator&() const { return this; }
};

实际验证示例

void test_empty_class() {
    EmptyClass obj1;              // 调用默认构造函数
    EmptyClass obj2(obj1);        // 调用拷贝构造函数
    EmptyClass obj3;
    obj3 = obj1;                  // 调用赋值运算符
    
    EmptyClass* ptr1 = &obj1;     // 调用取址运算符
    const EmptyClass* ptr2 = &obj1; // 调用const取址运算符
    
    // 析构函数在对象生命周期结束时自动调用
}

何时需要自定义这些函数

class ResourceClass {
private:
    int* data;
    size_t size;
    
public:
    // 必须自定义构造函数
    ResourceClass(size_t s) : size(s) {
        data = new int[size];
        cout << "构造函数:分配了 " << size << " 个整数的内存" << endl;
    }
    
    // 必须自定义拷贝构造函数(深拷贝)
    ResourceClass(const ResourceClass& other) : size(other.size) {
        data = new int[size];
        memcpy(data, other.data, size * sizeof(int));
        cout << "拷贝构造函数:深拷贝" << endl;
    }
    
    // 必须自定义赋值运算符
    ResourceClass& operator=(const ResourceClass& other) {
        if (this != &other) {  // 防止自赋值
            delete[] data;     // 释放原有资源
            
            size = other.size;
            data = new int[size];
            memcpy(data, other.data, size * sizeof(int));
            cout << "赋值运算符:深拷贝" << endl;
        }
        return *this;
    }
    
    // 必须自定义析构函数
    ~ResourceClass() {
        delete[] data;
        cout << "析构函数:释放内存" << endl;
    }
};

14. 拷贝构造函数 vs 赋值运算符

本质区别分析

调用时机不同
class TestClass {
public:
    int value;
    
    TestClass(int v) : value(v) {
        cout << "构造函数:创建对象,值=" << value << endl;
    }
    
    TestClass(const TestClass& other) : value(other.value) {
        cout << "拷贝构造函数:从现有对象创建新对象,值=" << value << endl;
    }
    
    TestClass& operator=(const TestClass& other) {
        cout << "赋值运算符:修改现有对象,从" << value << "改为" << other.value << endl;
        value = other.value;
        return *this;
    }
};

void copy_vs_assignment_demo() {
    TestClass obj1(10);           // 调用构造函数
    
    TestClass obj2(obj1);         // 调用拷贝构造函数(创建新对象)
    TestClass obj3 = obj1;        // 调用拷贝构造函数(不是赋值!)
    
    TestClass obj4(20);           // 调用构造函数
    obj4 = obj1;                  // 调用赋值运算符(修改现有对象)
}
内存管理的区别
class StringClass {
private:
    char* str;
    size_t length;
    
public:
    StringClass(const char* s) {
        length = strlen(s);
        str = new char[length + 1];
        strcpy(str, s);
        cout << "构造:" << str << endl;
    }
    
    // 拷贝构造函数:为新对象分配内存
    StringClass(const StringClass& other) {
        length = other.length;
        str = new char[length + 1];      // 分配新内存
        strcpy(str, other.str);
        cout << "拷贝构造:" << str << endl;
    }
    
    // 赋值运算符:需要处理现有内存
    StringClass& operator=(const StringClass& other) {
        if (this != &other) {            // 防止自赋值
            delete[] str;                // 释放原有内存
            
            length = other.length;
            str = new char[length + 1];  // 分配新内存
            strcpy(str, other.str);
            cout << "赋值:" << str << endl;
        }
        return *this;
    }
    
    ~StringClass() {
        cout << "析构:" << str << endl;
        delete[] str;
    }
};

自赋值问题的处理

StringClass& operator=(const StringClass& other) {
    // 方法1:检查自赋值
    if (this == &other) {
        return *this;
    }
    
    // 方法2:异常安全的实现
    char* temp = new char[other.length + 1];  // 先分配新内存
    strcpy(temp, other.str);
    
    delete[] str;        // 释放原内存
    str = temp;          // 指向新内存
    length = other.length;
    
    return *this;
}

15. 设计不可继承的类

使用模板和友元的方法

template <typename T>
class NonInheritable {
    friend T;  // 只有T类型可以访问私有构造函数
    
private:
    NonInheritable() {}   // 私有构造函数
    ~NonInheritable() {}  // 私有析构函数
};

// 可以实例化的类
class FinalClass : virtual public NonInheritable<FinalClass> {
public:
    FinalClass() {}   // 可以调用基类的私有构造函数(因为是友元)
    ~FinalClass() {}
};

// 尝试继承会失败的类
class AttemptInherit : public FinalClass {
public:
    AttemptInherit() {}  // 编译错误:无法访问NonInheritable的构造函数
    ~AttemptInherit() {}
};

void test_inheritance() {
    FinalClass obj;      // 正确:可以创建对象
    // AttemptInherit obj2; // 编译错误:无法继承
}

C++11的final关键字(推荐方法)

// 现代C++的简单方法
class FinalClass final {  // final关键字阻止继承
public:
    FinalClass() {
        cout << "FinalClass构造函数" << endl;
    }
    
    void do_something() {
        cout << "执行某些操作" << endl;
    }
};

// 编译错误:无法继承final类
// class DerivedClass : public FinalClass {};

void modern_final_demo() {
    FinalClass obj;
    obj.do_something();
}

16. 虚函数表机制深度解析

虚函数表的工作原理

class Base {
public:
    virtual void func1() { cout << "Base::func1" << endl; }
    virtual void func2() { cout << "Base::func2" << endl; }
    void func3() { cout << "Base::func3" << endl; }  // 非虚函数
};

class Derived : public Base {
public:
    virtual void func1() override { cout << "Derived::func1" << endl; }
    virtual void func4() { cout << "Derived::func4" << endl; }
};

内存布局分析

Base对象内存布局:
┌─────────────────┐
│   vptr (8字节)   │ ──→ Base虚函数表
└─────────────────┘     ┌─────────────────┐
                        │ &Base::func1    │
                        │ &Base::func2    │
                        └─────────────────┘

Derived对象内存布局:
┌─────────────────┐
│   vptr (8字节)   │ ──→ Derived虚函数表
└─────────────────┘     ┌─────────────────┐
                        │ &Derived::func1 │ (重写)
                        │ &Base::func2    │ (继承)
                        │ &Derived::func4 │ (新增)
                        └─────────────────┘

虚函数调用过程演示

void virtual_function_demo() {
    Base* ptr1 = new Base();
    Base* ptr2 = new Derived();
    
    // 虚函数调用:通过虚函数表
    ptr1->func1();  // 1. 获取ptr1的vptr
                    // 2. 在虚函数表中查找func1
                    // 3. 调用Base::func1
    
    ptr2->func1();  // 1. 获取ptr2的vptr
                    // 2. 在虚函数表中查找func1
                    // 3. 调用Derived::func1
    
    // 非虚函数调用:编译时确定
    ptr1->func3();  // 直接调用Base::func3
    ptr2->func3();  // 直接调用Base::func3
    
    delete ptr1;
    delete ptr2;
}

访问虚函数表的技巧(仅用于理解原理)

void access_vtable() {
    Derived obj;
    
    // 获取对象的虚函数表指针
    void** vtable = *(void***)&obj;
    
    // 调用虚函数表中的函数
    typedef void(*FuncPtr)();
    
    for (int i = 0; i < 3; i++) {
        FuncPtr func = (FuncPtr)vtable[i];
        cout << "调用虚函数表第" << i << "个函数:";
        // 注意:这种方式调用需要传递this指针,实际实现更复杂
    }
}

17. 函数重写、重载、隐藏的区别

重载(Overloading):同一作用域内的函数多态

class Calculator {
public:
    // 函数重载:函数名相同,参数不同
    int add(int a, int b) {
        cout << "两个整数相加" << endl;
        return a + b;
    }
    
    double add(double a, double b) {
        cout << "两个浮点数相加" << endl;
        return a + b;
    }
    
    int add(int a, int b, int c) {
        cout << "三个整数相加" << endl;
        return a + b + c;
    }
    
    // 编译错误:仅返回类型不同不能重载
    // double add(int a, int b) { return a + b; }
};

重写(Override):继承关系中的虚函数替换

class Shape {
public:
    virtual double area() {  // 虚函数
        cout << "Shape::area()" << endl;
        return 0.0;
    }
    
    virtual void draw() {    // 虚函数
        cout << "Shape::draw()" << endl;
    }
};

class Circle : public Shape {
private:
    double radius;
    
public:
    Circle(double r) : radius(r) {}
    
    // 重写基类虚函数
    virtual double area() override {
        cout << "Circle::area()" << endl;
        return 3.14159 * radius * radius;
    }
    
    virtual void draw() override {
        cout << "Circle::draw()" << endl;
    }
};

隐藏(Hiding):派生类函数隐藏基类同名函数

class Base {
public:
    void func() {
        cout << "Base::func()" << endl;
    }
    
    void func(int x) {
        cout << "Base::func(int)" << endl;
    }
    
    virtual void virtual_func() {
        cout << "Base::virtual_func()" << endl;
    }
};

class Derived : public Base {
public:
    // 隐藏基类的所有同名函数(包括重载版本)
    void func(double x) {
        cout << "Derived::func(double)" << endl;
    }
    
    // 隐藏基类虚函数(参数不同,不是重写)
    void virtual_func(int x) {
        cout << "Derived::virtual_func(int)" << endl;
    }
};

void hiding_demo() {
    Derived obj;
    
    obj.func(1.5);      // 调用Derived::func(double)
    // obj.func();      // 编译错误:Base::func()被隐藏
    // obj.func(1);     // 编译错误:Base::func(int)被隐藏
    
    // 使用作用域解析符访问被隐藏的函数
    obj.Base::func();   // 调用Base::func()
    obj.Base::func(1);  // 调用Base::func(int)
}

三者对比总结表

特征 重载(Overloading) 重写(Override) 隐藏(Hiding)
作用域 同一类中 基类和派生类 基类和派生类
函数名 相同 相同 相同
参数列表 必须不同 必须相同 可同可不同
virtual关键字 可有可无 基类必须有 可有可无
绑定时机 编译时 运行时 编译时
多态性 静态多态 动态多态 无多态

18. 多态实现原理:虚函数表详解

虚函数表的创建时机

class Animal {
public:
    Animal() {
        cout << "Animal构造函数:虚函数表指针已设置" << endl;
        // 此时vptr指向Animal的虚函数表
    }
    
    virtual void speak() {
        cout << "Animal发出声音" << endl;
    }
    
    virtual ~Animal() {
        cout << "Animal析构函数" << endl;
    }
};

class Dog : public Animal {
public:
    Dog() {
        cout << "Dog构造函数:虚函数表指针已更新" << endl;
        // 此时vptr指向Dog的虚函数表
    }
    
    virtual void speak() override {
        cout << "狗汪汪叫" << endl;
    }
    
    virtual ~Dog() {
        cout << "Dog析构函数" << endl;
    }
};

动态绑定的实现过程

void polymorphism_mechanism() {
    cout << "=== 多态机制演示 ===" << endl;
    
    Animal* animals[] = {
        new Animal(),
        new Dog()
    };
    
    for (int i = 0; i < 2; i++) {
        cout << "\n调用第" << i+1 << "个对象的speak()方法:" << endl;
        
        // 编译器生成的代码等价于:
        // 1. 获取对象的虚函数表指针
        // 2. 在虚函数表中查找speak函数的地址
        // 3. 调用该地址对应的函数
        animals[i]->speak();
    }
    
    // 清理内存
    for (int i = 0; i < 2; i++) {
        delete animals[i];  // 虚析构函数确保正确析构
    }
}

虚函数的性能开销

class PerformanceTest {
public:
    // 普通函数调用
    void normal_function() {
        // 直接函数调用,无额外开销
    }
    
    // 虚函数调用
    virtual void virtual_function() {
        // 需要通过虚函数表间接调用,有轻微开销
    }
};

void performance_comparison() {
    const int ITERATIONS = 10000000;
    PerformanceTest obj;
    PerformanceTest* ptr = &obj;
    
    // 测试普通函数调用性能
    auto start = chrono::high_resolution_clock::now();
    for (int i = 0; i < ITERATIONS; i++) {
        obj.normal_function();
    }
    auto end = chrono::high_resolution_clock::now();
    auto normal_time = chrono::duration_cast<chrono::microseconds>(end - start);
    
    // 测试虚函数调用性能
    start = chrono::high_resolution_clock::now();
    for (int i = 0; i < ITERATIONS; i++) {
        ptr->virtual_function();
    }
    end = chrono::high_resolution_clock::now();
    auto virtual_time = chrono::duration_cast<chrono::microseconds>(end - start);
    
    cout << "普通函数调用时间:" << normal_time.count() << "微秒" << endl;
    cout << "虚函数调用时间:" << virtual_time.count() << "微秒" << endl;
}

19. 数组 vs 链表:数据结构选择指南

内存布局对比

数组的连续存储
void array_memory_layout() {
    int arr[5] = {10, 20, 30, 40, 50};
    
    cout << "数组内存布局:" << endl;
    for (int i = 0; i < 5; i++) {
        cout << "arr[" << i << "] = " << arr[i] 
             << ", 地址:" << &arr[i] << endl;
    }
    
    // 地址连续,相邻元素地址差为sizeof(int)
}
链表的分散存储
struct ListNode {
    int data;           // 数据域
    ListNode* next;     // 指针域
    
    ListNode(int val) : data(val), next(nullptr) {}
};

class LinkedList {
private:
    ListNode* head;
    
public:
    LinkedList() : head(nullptr) {}
    
    void insert(int val) {
        ListNode* new_node = new ListNode(val);  // 动态分配,地址不连续
        new_node->next = head;
        head = new_node;
    }
    
    void print_addresses() {
        cout << "链表节点地址:" << endl;
        ListNode* current = head;
        int index = 0;
        while (current) {
            cout << "节点" << index << ": 数据=" << current->data 
                 << ", 地址=" << current << endl;
            current = current->next;
            index++;
        }
    }
    
    ~LinkedList() {
        while (head) {
            ListNode* temp = head;
            head = head->next;
            delete temp;
        }
    }
};

操作性能对比

随机访问性能
void random_access_test() {
    const int SIZE = 100000;
    
    // 数组随机访问:O(1)
    vector<int> arr(SIZE);
    for (int i = 0; i < SIZE; i++) {
        arr[i] = i;
    }
    
    auto start = chrono::high_resolution_clock::now();
    for (int i = 0; i < 10000; i++) {
        int index = rand() % SIZE;
        int value = arr[index];  // 直接通过索引访问
    }
    auto end = chrono::high_resolution_clock::now();
    auto array_time = chrono::duration_cast<chrono::microseconds>(end - start);
    
    // 链表随机访问:O(n)
    LinkedList list;
    for (int i = 0; i < SIZE; i++) {
        list.insert(i);
    }
    
    start = chrono::high_resolution_clock::now();
    for (int i = 0; i < 100; i++) {  // 减少测试次数,因为链表访问很慢
        int index = rand() % SIZE;
        // 需要从头遍历到指定位置
        ListNode* current = list.head;
        for (int j = 0; j < index && current; j++) {
            current = current->next;
        }
    }
    end = chrono::high_resolution_clock::now();
    auto list_time = chrono::duration_cast<chrono::microseconds>(end - start);
    
    cout << "数组随机访问时间:" << array_time.count() << "微秒" << endl;
    cout << "链表随机访问时间:" << list_time.count() << "微秒" << endl;
}

C/C++面试核心知识点详解

1. 变量的声明与定义:内存分配的本质区别

核心概念

在C/C++中,变量的声明定义是两个完全不同的概念:

  • 声明(Declaration):告诉编译器变量的名称和类型,但不分配内存空间
  • 定义(Definition):不仅声明变量,还为其分配实际的内存空间

实际应用示例

// 变量声明:使用extern关键字,不分配内存
extern int global_var;  // 声明一个整型变量,告诉编译器这个变量在别处定义

// 变量定义:分配内存空间
int global_var = 100;   // 定义变量并分配内存,可以初始化

// 函数声明
int add(int a, int b);  // 函数声明,不包含函数体

// 函数定义
int add(int a, int b) { // 函数定义,包含完整实现
    return a + b;
}

重要规则

  • 一个变量可以在多个地方声明,但只能在一个地方定义
  • 声明可以重复,定义不能重复
  • 使用变量前必须先声明,使用时才需要定义

2. 不同数据类型与零值比较的标准写法

为什么要规范比较写法?

不同数据类型与零值比较时,写法不当可能导致逻辑错误或程序崩溃。

标准比较方式

bool类型:直接判断真假
bool is_valid = true;

// 正确写法:直接使用布尔值
if (is_valid) {
    // 条件为真时执行
    printf("数据有效\n");
} else {
    // 条件为假时执行
    printf("数据无效\n");
}
int类型:与0比较
int count = 10;

// 正确写法:将常量放在左边(防御性编程)
if (0 != count) {
    // count不等于0时执行
    printf("计数值为:%d\n", count);
} else {
    // count等于0时执行
    printf("计数为零\n");
}
指针类型:与NULL比较
int* ptr = nullptr;

// 正确写法:将NULL放在左边
if (NULL == ptr) {
    // 指针为空时执行
    printf("指针为空\n");
} else {
    // 指针不为空时执行
    printf("指针指向的值:%d\n", *ptr);
}
float类型:使用精度范围比较
float value = 0.0001f;
const float EPSILON = 1e-6f;  // 定义精度阈值

// 正确写法:判断是否在精度范围内
if ((value >= -EPSILON) && (value <= EPSILON)) {
    // 认为等于零
    printf("浮点数接近零\n");
} else {
    // 不等于零
    printf("浮点数值:%f\n", value);
}

防御性编程技巧

将常量放在比较运算符左边的好处:

// 错误示例:容易写错
if (count = 0) {  // 误将==写成=,编译通过但逻辑错误
    // 永远不会执行
}

// 正确示例:编译器会报错
if (0 = count) {  // 编译错误,无法给常量赋值
    // 编译器直接报错,避免逻辑错误
}

3. sizeof与strlen:编译时计算 vs 运行时计算

本质区别分析

sizeof:编译时操作符
// sizeof是操作符,不是函数
int arr[10];
char str[] = "Hello";

// 编译时就确定结果
size_t arr_size = sizeof(arr);      // 结果:40字节(10个int)
size_t str_size = sizeof(str);      // 结果:6字节(包含'\0')
size_t int_size = sizeof(int);      // 结果:4字节(平台相关)
strlen:运行时库函数
#include <string.h>

char str[] = "Hello";
char* ptr = "World";

// 运行时计算字符串长度
size_t len1 = strlen(str);  // 结果:5(不包含'\0')
size_t len2 = strlen(ptr);  // 结果:5(不包含'\0')

数组退化现象

void test_array(char arr[]) {
    // 数组作为参数时退化为指针
    printf("sizeof(arr) = %zu\n", sizeof(arr));    // 输出指针大小(8字节)
    printf("strlen(arr) = %zu\n", strlen(arr));    // 输出字符串长度
}

int main() {
    char str[] = "Hello";
    printf("sizeof(str) = %zu\n", sizeof(str));    // 输出:6
    printf("strlen(str) = %zu\n", strlen(str));    // 输出:5
    
    test_array(str);  // 数组退化为指针
    return 0;
}

性能对比

  • sizeof:编译时确定,零运行时开销
  • strlen:需要遍历字符串,时间复杂度O(n)

4. static关键字:C语言 vs C++的功能扩展

C语言中的static

// 1. 局部静态变量:函数调用间保持值
void counter() {
    static int count = 0;  // 只初始化一次
    count++;
    printf("调用次数:%d\n", count);
}

// 2. 全局静态变量:限制作用域在当前文件
static int file_global = 100;  // 只在当前文件可见

// 3. 静态函数:限制函数作用域在当前文件
static void helper_function() {
    printf("这是一个静态函数\n");
}

C++中的static扩展功能

class MyClass {
private:
    static int class_count;     // 静态成员变量:所有对象共享
    int instance_id;            // 实例成员变量:每个对象独有

public:
    MyClass() {
        instance_id = ++class_count;  // 每创建一个对象,计数加1
    }
    
    // 静态成员函数:不依赖具体对象实例
    static int get_count() {
        return class_count;     // 只能访问静态成员
        // return instance_id;  // 错误:无法访问非静态成员
    }
};

// 静态成员变量必须在类外定义
int MyClass::class_count = 0;

// 使用示例
int main() {
    MyClass obj1, obj2, obj3;
    printf("创建的对象数量:%d\n", MyClass::get_count());  // 输出:3
    return 0;
}

静态变量的内存特点

  • 存储在静态存储区,程序结束时才销毁
  • 只初始化一次,后续调用保持上次的值
  • 可以在不同函数调用间传递信息

5. malloc vs new:C风格 vs C++风格的内存管理

基本区别对比

malloc/free:C语言风格
#include <stdlib.h>

// 分配内存
int* ptr = (int*)malloc(sizeof(int) * 10);  // 分配10个int的空间
if (ptr == NULL) {
    printf("内存分配失败\n");
    return -1;
}

// 使用内存
for (int i = 0; i < 10; i++) {
    ptr[i] = i * i;  // 需要手动初始化
}

// 释放内存
free(ptr);      // 只释放内存,不调用析构函数
ptr = NULL;     // 防止悬空指针
new/delete:C++风格
// 分配单个对象
int* single_ptr = new int(42);          // 分配并初始化
delete single_ptr;                      // 释放单个对象

// 分配数组
int* array_ptr = new int[10];           // 分配数组
delete[] array_ptr;                     // 释放数组(注意使用delete[])

// 分配类对象
class Person {
public:
    Person(const char* name) {
        printf("构造函数:创建 %s\n", name);
    }
    ~Person() {
        printf("析构函数:销毁对象\n");
    }
};

Person* person = new Person("张三");     // 自动调用构造函数
delete person;                          // 自动调用析构函数

构造函数与析构函数的区别

class TestClass {
public:
    TestClass() { 
        printf("对象被构造\n"); 
        data = new int[100];  // 分配资源
    }
    ~TestClass() { 
        printf("对象被析构\n"); 
        delete[] data;        // 释放资源
    }
private:
    int* data;
};

// malloc方式:不会调用构造/析构函数
TestClass* obj1 = (TestClass*)malloc(sizeof(TestClass));
// 没有输出"对象被构造"
free(obj1);  // 没有输出"对象被析构",可能导致内存泄漏

// new方式:自动调用构造/析构函数
TestClass* obj2 = new TestClass();  // 输出"对象被构造"
delete obj2;                        // 输出"对象被析构"

混用的危险性

// 错误示例:不要混用
int* ptr1 = (int*)malloc(sizeof(int));
delete ptr1;    // 错误:malloc的内存不能用delete释放

int* ptr2 = new int;
free(ptr2);     // 错误:new的内存不能用free释放

6. 宏定义的陷阱:副作用问题

标准MIN宏的实现

#define MIN(a, b) ((a) <= (b) ? (a) : (b))

// 基本使用
int x = 5, y = 3;
int min_val = MIN(x, y);  // 结果:3

宏的副作用问题

#define MIN(a, b) ((a) <= (b) ? (a) : (b))

int main() {
    int x = 5;
    int* p = &x;
    
    // 危险的调用方式
    int result = MIN(++(*p), 10);
    
    // 宏展开后变成:
    // int result = ((++(*p)) <= (10) ? (++(*p)) : (10));
    // ++(*p)被执行了两次!
    
    printf("x = %d\n", x);      // x可能是7而不是6
    printf("result = %d\n", result);
    
    return 0;
}

更安全的实现方式

// 使用内联函数替代宏(C++推荐)
inline int min_safe(int a, int b) {
    return (a <= b) ? a : b;
}

// 或者使用临时变量的宏(C语言)
#define MIN_SAFE(a, b) ({ \
    typeof(a) _a = (a); \
    typeof(b) _b = (b); \
    (_a <= _b) ? _a : _b; \
})

7. volatile关键字:处理不可预测的变量变化

volatile的作用机制

volatile告诉编译器:这个变量可能被程序外部因素修改,不要进行优化。

典型应用场景

中断服务程序
volatile int interrupt_flag = 0;  // 中断标志

// 中断服务函数
void interrupt_handler() {
    interrupt_flag = 1;  // 中断发生时设置标志
}

// 主程序
int main() {
    while (interrupt_flag == 0) {
        // 等待中断发生
        // 如果没有volatile,编译器可能优化成死循环
    }
    printf("中断已处理\n");
    return 0;
}
硬件寄存器访问
// 硬件寄存器地址
volatile unsigned int* const HARDWARE_REG = (unsigned int*)0x40000000;

void read_sensor() {
    unsigned int value = *HARDWARE_REG;  // 每次都从硬件读取
    printf("传感器值:%u\n", value);
}
多线程共享变量
volatile bool thread_running = true;

void worker_thread() {
    while (thread_running) {  // 确保每次都检查最新值
        // 执行工作
        printf("线程运行中...\n");
        sleep(1);
    }
    printf("线程退出\n");
}

void stop_thread() {
    thread_running = false;  // 通知线程停止
}

volatile指针的不同含义

int value = 100;

// 指向volatile变量的指针
volatile int* ptr1 = &value;        // 指向的内容是volatile的
*ptr1 = 200;                        // 每次写入都不会被优化

// volatile指针指向普通变量
int* volatile ptr2 = &value;        // 指针本身是volatile的
ptr2 = &another_value;              // 指针的修改不会被优化

// volatile指针指向volatile变量
volatile int* volatile ptr3 = &value; // 指针和内容都是volatile的

8. 数组名与数组地址:a vs &a的本质区别

概念解析

  • a:数组名,表示数组首元素的地址
  • &a:数组的地址,表示整个数组的地址

代码分析实例

#include <stdio.h>

int main() {
    int a[5] = {1, 2, 3, 4, 5};
    
    printf("a = %p\n", a);          // 数组首元素地址
    printf("&a = %p\n", &a);        // 整个数组的地址
    printf("a+1 = %p\n", a+1);      // 下一个元素地址(+4字节)
    printf("&a+1 = %p\n", &a+1);    // 下一个数组地址(+20字节)
    
    // 关键代码分析
    int* ptr = (int*)(&a + 1);      // 指向数组后面的位置
    printf("*(a+1) = %d\n", *(a+1));    // 输出:2(第二个元素)
    printf("*(ptr-1) = %d\n", *(ptr-1)); // 输出:5(最后一个元素)
    
    return 0;
}

内存布局图解

内存地址:  1000   1004   1008   1012   1016   1020
数组内容:  [ 1 ]  [ 2 ]  [ 3 ]  [ 4 ]  [ 5 ]
          ↑                              ↑      ↑
          a                              |      &a+1
          &a                             |
                                    a+4或&a[4]

指针运算的区别

int a[5] = {1, 2, 3, 4, 5};

// a+1:移动一个int的大小(4字节)
int* p1 = a + 1;        // 指向a[1]

// &a+1:移动整个数组的大小(20字节)
int* p2 = (int*)(&a + 1); // 指向数组后面的位置

// 验证
printf("p1指向的值:%d\n", *p1);      // 输出:2
printf("p2-1指向的值:%d\n", *(p2-1)); // 输出:5

9. C/C++程序内存布局:五大存储区域详解

内存分区概述

C/C++程序运行时,内存被划分为5个主要区域,每个区域有不同的特点和用途。

1. 程序代码区(Text Segment)

// 存储编译后的机器代码
void function1() {
    printf("这个函数的代码存储在代码区\n");
}

int main() {
    printf("main函数的代码也在代码区\n");
    return 0;
}

特点:只读、共享、程序加载时确定大小

2. 全局/静态存储区(Data Segment)

// 已初始化的全局变量
int global_var = 100;           // 存储在已初始化数据区

// 未初始化的全局变量
int uninitialized_global;       // 存储在BSS区(自动初始化为0)

// 静态变量
static int static_var = 200;    // 存储在已初始化数据区

void function() {
    static int local_static;    // 存储在BSS区
}

特点:程序运行期间一直存在、自动初始化为0(BSS区)

3. 栈区(Stack)

void stack_demo() {
    int local_var = 10;         // 局部变量,存储在栈上
    char buffer[1024];          // 局部数组,存储在栈上
    
    printf("local_var地址:%p\n", &local_var);
    printf("buffer地址:%p\n", buffer);
    
    // 函数结束时,这些变量自动销毁
}

void recursive_function(int n) {
    int local = n;              // 每次递归调用都在栈上分配
    if (n > 0) {
        recursive_function(n - 1);
    }
}

特点

  • 自动管理(函数结束自动释放)
  • 访问速度快
  • 大小有限(通常几MB)
  • 后进先出(LIFO)

4. 堆区(Heap)

void heap_demo() {
    // 动态分配内存
    int* heap_ptr = (int*)malloc(sizeof(int) * 100);
    if (heap_ptr == NULL) {
        printf("内存分配失败\n");
        return;
    }
    
    // 使用堆内存
    for (int i = 0; i < 100; i++) {
        heap_ptr[i] = i;
    }
    
    // 必须手动释放
    free(heap_ptr);
    heap_ptr = NULL;  // 防止悬空指针
}

void cpp_heap_demo() {
    // C++风格的堆内存管理
    int* ptr = new int[100];    // 分配
    // 使用内存...
    delete[] ptr;               // 释放
}

特点

  • 手动管理(程序员负责分配和释放)
  • 大小灵活
  • 访问速度相对较慢
  • 容易产生内存泄漏和碎片

5. 文字常量区(String Literal Pool)

void string_demo() {
    char* str1 = "Hello World";     // 字符串存储在常量区
    char* str2 = "Hello World";     // 可能与str1指向同一地址
    
    char arr[] = "Hello World";     // 字符串复制到栈上
    
    printf("str1地址:%p\n", str1);
    printf("str2地址:%p\n", str2);
    printf("arr地址:%p\n", arr);
    
    // str1[0] = 'h';  // 错误:不能修改常量区内容
    arr[0] = 'h';      // 正确:可以修改栈上的副本
}

内存布局示意图

高地址
┌─────────────────┐
│     栈区        │ ← 向下增长
│   (局部变量)     │
├─────────────────┤
│       ↓         │
│                 │
│       ↑         │
├─────────────────┤
│     堆区        │ ← 向上增长
│   (动态分配)     │
├─────────────────┤
│   未初始化数据   │
│    (BSS段)      │
├─────────────────┤
│   已初始化数据   │
│   (Data段)      │
├─────────────────┤
│   文字常量区     │
├─────────────────┤
│    程序代码区    │
└─────────────────┘
低地址

10. 字符串操作函数对比:strcpy、sprintf、memcpy

功能定位分析

strcpy:字符串到字符串的复制
#include <string.h>

void strcpy_demo() {
    char source[] = "Hello World";
    char destination[20];
    
    // 复制字符串(包括结尾的'\0')
    strcpy(destination, source);
    printf("复制结果:%s\n", destination);
    
    // 注意:不检查目标缓冲区大小,可能溢出
    // 更安全的版本:strncpy
    strncpy(destination, source, sizeof(destination) - 1);
    destination[sizeof(destination) - 1] = '\0';  // 确保以'\0'结尾
}
sprintf:格式化输出到字符串
#include <stdio.h>

void sprintf_demo() {
    char buffer[100];
    int age = 25;
    float height = 175.5f;
    char name[] = "张三";
    
    // 将多种数据类型格式化为字符串
    sprintf(buffer, "姓名:%s,年龄:%d,身高:%.1f厘米", 
            name, age, height);
    
    printf("格式化结果:%s\n", buffer);
    
    // 更安全的版本:snprintf
    snprintf(buffer, sizeof(buffer), "安全的格式化:%s", name);
}
memcpy:内存块到内存块的复制
#include <string.h>

void memcpy_demo() {
    // 复制整数数组
    int source[] = {1, 2, 3, 4, 5};
    int destination[5];
    
    memcpy(destination, source, sizeof(source));
    
    // 复制结构体
    struct Person {
        char name[20];
        int age;
    };
    
    struct Person p1 = {"李四", 30};
    struct Person p2;
    
    memcpy(&p2, &p1, sizeof(struct Person));
    printf("复制的结构体:%s, %d\n", p2.name, p2.age);
    
    // 复制部分内存
    char str1[] = "Hello World";
    char str2[20];
    memcpy(str2, str1, 5);  // 只复制前5个字符
    str2[5] = '\0';         // 手动添加结束符
    printf("部分复制:%s\n", str2);  // 输出:Hello
}

性能对比测试

#include <time.h>

void performance_test() {
    const int TEST_SIZE = 1000000;
    char source[1000];
    char destination[1000];
    clock_t start, end;
    
    // 初始化源数据
    memset(source, 'A', sizeof(source) - 1);
    source[sizeof(source) - 1] = '\0';
    
    // 测试memcpy性能
    start = clock();
    for (int i = 0; i < TEST_SIZE; i++) {
        memcpy(destination, source, sizeof(source));
    }
    end = clock();
    printf("memcpy耗时:%f秒\n", (double)(end - start) / CLOCKS_PER_SEC);
    
    // 测试strcpy性能
    start = clock();
    for (int i = 0; i < TEST_SIZE; i++) {
        strcpy(destination, source);
    }
    end = clock();
    printf("strcpy耗时:%f秒\n", (double)(end - start) / CLOCKS_PER_SEC);
    
    // 测试sprintf性能
    start = clock();
    for (int i = 0; i < TEST_SIZE; i++) {
        sprintf(destination, "%s", source);
    }
    end = clock();
    printf("sprintf耗时:%f秒\n", (double)(end - start) / CLOCKS_PER_SEC);
}

使用场景选择指南

  • strcpy:纯字符串复制,需要自动处理’\0’结尾
  • sprintf:需要格式化多种数据类型为字符串
  • memcpy:原始内存复制,最高效,适合大块数据

11. 直接内存操作:指定地址赋值技术

基本概念

在嵌入式开发或系统编程中,经常需要直接操作特定内存地址的数据。

实现方法

void memory_operation_demo() {
    // 将整数值0xaa66写入地址0x67a9
    int* ptr;                    // 声明整型指针
    ptr = (int*)0x67a9;         // 将地址强制转换为整型指针
    *ptr = 0xaa66;              // 向该地址写入数据
    
    // 读取验证
    printf("地址0x67a9的值:0x%x\n", *ptr);
}

实际应用场景

硬件寄存器操作
// 定义硬件寄存器地址
#define GPIO_BASE_ADDR    0x40020000
#define GPIO_OUTPUT_REG   (GPIO_BASE_ADDR + 0x14)
#define GPIO_INPUT_REG    (GPIO_BASE_ADDR + 0x10)

void gpio_control() {
    // 控制GPIO输出
    volatile unsigned int* gpio_output = (volatile unsigned int*)GPIO_OUTPUT_REG;
    *gpio_output = 0xFF;  // 设置所有引脚为高电平
    
    // 读取GPIO输入
    volatile unsigned int* gpio_input = (volatile unsigned int*)GPIO_INPUT_REG;
    unsigned int input_value = *gpio_input;
    printf("GPIO输入值:0x%x\n", input_value);
}
内存映射文件操作
#include <sys/mman.h>
#include <fcntl.h>

void memory_mapped_file() {
    int fd = open("data.bin", O_RDWR);
    if (fd == -1) return;
    
    // 将文件映射到内存
    void* mapped_addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, 
                            MAP_SHARED, fd, 0);
    
    if (mapped_addr != MAP_FAILED) {
        // 直接操作内存就是操作文件
        int* data_ptr = (int*)mapped_addr;
        *data_ptr = 0x12345678;  // 写入数据到文件
        
        // 解除映射
        munmap(mapped_addr, 1024);
    }
    
    close(fd);
}

安全注意事项

void safe_memory_access() {
    // 1. 检查地址有效性
    void* addr = (void*)0x67a9;
    if (addr == NULL) {
        printf("无效地址\n");
        return;
    }
    
    // 2. 使用volatile防止编译器优化
    volatile int* ptr = (volatile int*)addr;
    
    // 3. 异常处理(在支持的系统上)
    try {
        *ptr = 0xaa66;
    } catch (...) {
        printf("内存访问异常\n");
    }
}

12. 面向对象三大特征深度解析

1. 封装性(Encapsulation):数据隐藏与接口设计

基本概念

封装是将数据和操作数据的方法组合在一起,通过访问控制来隐藏内部实现细节。

class BankAccount {
private:
    double balance;          // 私有数据:外部无法直接访问
    string account_number;   // 私有数据:账户安全信息
    
public:
    // 公有接口:提供安全的访问方式
    BankAccount(string acc_num, double initial_balance) {
        account_number = acc_num;
        balance = initial_balance;
    }
    
    // 存款操作:控制数据修改方式
    bool deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            return true;
        }
        return false;  // 拒绝无效操作
    }
    
    // 取款操作:包含业务逻辑验证
    bool withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;  // 余额不足或金额无效
    }
    
    // 查询余额:只读访问
    double get_balance() const {
        return balance;
    }
};

2. 继承性(Inheritance):代码复用与层次结构

// 基类:动物
class Animal {
protected:
    string name;
    int age;
    
public:
    Animal(string n, int a) : name(n), age(a) {}
    
    // 虚函数:允许子类重写
    virtual void make_sound() {
        cout << name << "发出声音" << endl;
    }
    
    virtual ~Animal() {}  // 虚析构函数
};

// 派生类:狗
class Dog : public Animal {
public:
    Dog(string n, int a) : Animal(n, a) {}
    
    // 重写基类的虚函数
    virtual void make_sound() override {
        cout << name << "汪汪叫" << endl;
    }
};

3. 多态性(Polymorphism):一个接口多种实现

void demonstrate_polymorphism() {
    // 创建不同类型的动物对象
    Animal* animals[] = {
        new Dog("旺财", 3),
        new Animal("未知动物", 2)
    };
    
    // 多态调用:同一接口,不同实现
    for (int i = 0; i < 2; i++) {
        animals[i]->make_sound();  // 根据实际对象类型调用相应函数
    }
    
    // 清理内存
    for (int i = 0; i < 2; i++) {
        delete animals[i];
    }
}

13. C++空类的隐式成员函数

编译器自动生成的六个函数

class EmptyClass {
    // 编译器自动生成以下函数:
    // 1. 默认构造函数
    // 2. 拷贝构造函数
    // 3. 析构函数
    // 4. 赋值运算符
    // 5. 取址运算符
    // 6. const取址运算符
};

14. 拷贝构造函数 vs 赋值运算符

调用时机不同

class TestClass {
public:
    int value;
    
    TestClass(int v) : value(v) {}
    
    TestClass(const TestClass& other) : value(other.value) {
        cout << "拷贝构造函数调用" << endl;
    }
    
    TestClass& operator=(const TestClass& other) {
        cout << "赋值运算符调用" << endl;
        value = other.value;
        return *this;
    }
};

void demo() {
    TestClass obj1(10);           // 构造函数
    TestClass obj2(obj1);         // 拷贝构造函数
    TestClass obj3 = obj1;        // 拷贝构造函数(不是赋值!)
    
    TestClass obj4(20);           // 构造函数
    obj4 = obj1;                  // 赋值运算符
}

15. 设计不可继承的类

C++11的final关键字(推荐方法)

class FinalClass final {  // final关键字阻止继承
public:
    FinalClass() {
        cout << "FinalClass构造函数" << endl;
    }
    
    void do_something() {
        cout << "执行某些操作" << endl;
    }
};

// 编译错误:无法继承final类
// class DerivedClass : public FinalClass {};

16. 虚函数表机制深度解析

虚函数表的工作原理

class Base {
public:
    virtual void func1() { cout << "Base::func1" << endl; }
    virtual void func2() { cout << "Base::func2" << endl; }
};

class Derived : public Base {
public:
    virtual void func1() override { cout << "Derived::func1" << endl; }
};

void virtual_demo() {
    Base* ptr = new Derived();
    ptr->func1();  // 通过虚函数表调用Derived::func1
    delete ptr;
}

17. 函数重写、重载、隐藏的区别

重载(Overloading):同一作用域内的函数多态

class Calculator {
public:
    int add(int a, int b) { return a + b; }           // 整数版本
    double add(double a, double b) { return a + b; }  // 浮点版本
    int add(int a, int b, int c) { return a + b + c; } // 三参数版本
};

重写(Override):继承关系中的虚函数替换

class Shape {
public:
    virtual double area() { return 0.0; }  // 基类虚函数
};

class Circle : public Shape {
public:
    virtual double area() override {       // 重写基类虚函数
        return 3.14159 * radius * radius;
    }
private:
    double radius;
};

18. 多态实现原理:虚函数表详解

动态绑定的实现过程

void polymorphism_mechanism() {
    Shape* shapes[] = {
        new Circle(5.0),
        new Rectangle(4.0, 6.0)
    };
    
    for (int i = 0; i < 2; i++) {
        // 编译器生成的代码:
        // 1. 获取对象的虚函数表指针
        // 2. 在虚函数表中查找area函数的地址
        // 3. 调用该地址对应的函数
        cout << "面积:" << shapes[i]->area() << endl;
    }
    
    for (int i = 0; i < 2; i++) {
        delete shapes[i];
    }
}

19. 数组 vs 链表:数据结构选择指南

性能对比分析

数组的优势
void array_advantages() {
    int arr[1000];
    
    // 1. 随机访问:O(1)时间复杂度
    int value = arr[500];  // 直接通过索引访问
    
    // 2. 内存连续,缓存友好
    for (int i = 0; i < 1000; i++) {
        arr[i] = i * i;  // 顺序访问,缓存命中率高
    }
    
    // 3. 空间效率高:只存储数据,无额外指针开销
}
链表的优势
struct ListNode {
    int data;
    ListNode* next;
    
    ListNode(int val) : data(val), next(nullptr) {}
};

class LinkedList {
private:
    ListNode* head;
    
public:
    LinkedList() : head(nullptr) {}
    
    // 1. 插入操作:O(1)时间复杂度(在已知位置)
    void insert_at_head(int val) {
        ListNode* new_node = new ListNode(val);
        new_node->next = head;
        head = new_node;
    }
    
    // 2. 删除操作:O(1)时间复杂度(在已知位置)
    void delete_node(ListNode* prev, ListNode* current) {
        if (prev) {
            prev->next = current->next;
        } else {
            head = current->next;
        }
        delete current;
    }
    
    // 3. 动态大小:可以根据需要增长或缩小
    void dynamic_resize() {
        // 链表大小可以动态变化,不需要预先分配固定大小
    }
};

使用场景选择指南

  • 数组适用场景

    • 需要频繁随机访问元素
    • 数据大小相对固定
    • 对内存使用效率要求高
    • 需要利用CPU缓存优化性能
  • 链表适用场景

    • 需要频繁插入和删除操作
    • 数据大小变化很大
    • 不需要随机访问元素
    • 内存分配需要灵活性

20. 单链表反转算法实现

迭代算法实现

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

// 迭代方法反转链表
ListNode* reverse_iterative(ListNode* head) {
    if (!head) {                    // 判断链表是否为空
        return head;
    }
    
    ListNode* prev = nullptr;       // 前一个节点指针
    ListNode* current = head;       // 当前节点指针
    ListNode* next = nullptr;       // 下一个节点指针
    
    while (current != nullptr) {    // 遍历整个链表
        next = current->next;       // 保存下一个节点
        current->next = prev;       // 反转当前节点的指针
        prev = current;             // 移动prev指针
        current = next;             // 移动current指针
    }
    
    return prev;                    // prev现在指向新的头节点
}

递归算法实现

// 递归方法反转链表
ListNode* reverse_recursive(ListNode* head) {
    // 基础情况:空链表或只有一个节点
    if (!head || !head->next) {
        return head;
    }
    
    // 递归反转剩余部分
    ListNode* new_head = reverse_recursive(head->next);
    
    // 反转当前连接
    head->next->next = head;        // 将下一个节点指向当前节点
    head->next = nullptr;           // 当前节点指向空
    
    return new_head;                // 返回新的头节点
}

完整测试示例

// 创建链表的辅助函数
ListNode* create_list(vector<int>& values) {
    if (values.empty()) return nullptr;
    
    ListNode* head = new ListNode(values[0]);
    ListNode* current = head;
    
    for (int i = 1; i < values.size(); i++) {
        current->next = new ListNode(values[i]);
        current = current->next;
    }
    
    return head;
}

// 打印链表的辅助函数
void print_list(ListNode* head) {
    ListNode* current = head;
    while (current) {
        cout << current->val;
        if (current->next) cout << " -> ";
        current = current->next;
    }
    cout << " -> NULL" << endl;
}

// 测试函数
void test_reverse() {
    vector<int> values = {1, 2, 3, 4, 5};
    
    // 测试迭代方法
    ListNode* list1 = create_list(values);
    cout << "原始链表:";
    print_list(list1);
    
    ListNode* reversed1 = reverse_iterative(list1);
    cout << "迭代反转后:";
    print_list(reversed1);
    
    // 测试递归方法
    ListNode* list2 = create_list(values);
    ListNode* reversed2 = reverse_recursive(list2);
    cout << "递归反转后:";
    print_list(reversed2);
}

算法复杂度分析

  • 时间复杂度:O(n),需要遍历链表中的每个节点一次
  • 空间复杂度
    • 迭代方法:O(1),只使用常数额外空间
    • 递归方法:O(n),递归调用栈的深度为n

实际应用场景

  1. 数据结构操作:在实现栈、队列等数据结构时需要反转操作
  2. 算法题解决:许多算法问题需要链表反转作为子问题
  3. 系统设计:在某些系统中需要反转数据流或操作序列

总结

本文详细介绍了C/C++20个核心知识点,涵盖了从基础语法到高级特性的各个方面:

  1. 基础概念:变量声明定义、数据类型比较、sizeof/strlen区别
  2. 内存管理:static关键字、malloc/new区别、内存布局
  3. 面向对象:三大特征、虚函数机制、多态实现
  4. 数据结构:数组链表对比、链表反转算法
  5. 编程技巧:宏定义陷阱、volatile关键字、防御性编程

掌握这些知识点不仅能帮助你在面试中脱颖而出,更重要的是能提升你的编程能力和代码质量。建议读者结合实际项目练习,加深对这些概念的理解和应用。

记住:理论知识要与实践相结合,多写代码、多调试、多思考,才能真正掌握C/C++编程的精髓。


网站公告

今日签到

点亮在社区的每一天
去签到