当前位置: 首页 > news >正文

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

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));    // 输出:6printf("strlen(str) = %zu\n", strlen(str));    // 输出:5test_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());  // 输出:3return 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而不是6printf("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);// 注意:不检查目标缓冲区大小,可能溢出// 更安全的版本:strncpystrncpy(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);// 更安全的版本:snprintfsnprintf(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写入地址0x67a9int* 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::func1ptr2->func1();  // 1. 获取ptr2的vptr// 2. 在虚函数表中查找func1// 3. 调用Derived::func1// 非虚函数调用:编译时确定ptr1->func3();  // 直接调用Base::func3ptr2->func3();  // 直接调用Base::func3delete 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));    // 输出:6printf("strlen(str) = %zu\n", strlen(str));    // 输出:5test_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());  // 输出:3return 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而不是6printf("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);// 注意:不检查目标缓冲区大小,可能溢出// 更安全的版本:strncpystrncpy(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);// 更安全的版本:snprintfsnprintf(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写入地址0x67a9int* 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::func1delete 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++编程的精髓。

http://www.xdnf.cn/news/1200961.html

相关文章:

  • Qt C++ GUI 函数参数速查手册:基础与布局
  • RK3568 Linux驱动学习——Linux驱动开发准备工作
  • 【科研绘图系列】R语言绘制边际云雨图散点图
  • 基于大模型的预训练、量化、微调等完整流程解析
  • rust-模块树中引用项的路径
  • 1439-素数环2
  • 扩展组件(uni-ui)之uni-group
  • 硅基计划3.0 学习总结 肆 二叉树 初版
  • 疯狂星期四文案网第21天运营日记
  • 剑指offer第2版:双指针+排序+分治+滑动窗口
  • QT6 源,七章对话框与多窗体(17)用于辅助多文档 MDI 窗体设计 QMdiArea 的类 QMdiSubWindow:
  • MySQL 8.4 Windows 版安装记录与步骤参考
  • 《频率之光:群星之我》
  • mmap的调用层级与内核态陷入全过程
  • 依赖倒置原则 Dependency Inversion Principle - DIP
  • 不坑盒子突然不见了怎么办?
  • VILA系列论文解读
  • 详细解释一个ros的CMakeLists.txt文件
  • AI大模型前沿:Muyan-TTS开源零样本语音合成技术解析
  • 自然语言处理NLP (1)
  • 【I】题目解析
  • vmware虚拟机中显示“网络电缆被拔出“的解决方法
  • rust-包和箱子
  • RHEL9 网络配置入门:IP 显示、主机名修改与配置文件解析
  • 电动汽车转向系统及其工作原理
  • 8.c语言指针
  • Web开发系列-第0章 Web介绍
  • SQL注入SQLi-LABS 靶场less21-25详细通关攻略
  • Ubuntu普通用户环境异常问题
  • 数学建模——灰色关联分析