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
实际应用场景
- 数据结构操作:在实现栈、队列等数据结构时需要反转操作
- 算法题解决:许多算法问题需要链表反转作为子问题
- 系统设计:在某些系统中需要反转数据流或操作序列
总结
本文详细介绍了C/C++20个核心知识点,涵盖了从基础语法到高级特性的各个方面:
- 基础概念:变量声明定义、数据类型比较、sizeof/strlen区别
- 内存管理:static关键字、malloc/new区别、内存布局
- 面向对象:三大特征、虚函数机制、多态实现
- 数据结构:数组链表对比、链表反转算法
- 编程技巧:宏定义陷阱、volatile关键字、防御性编程
掌握这些知识点不仅能帮助你在面试中脱颖而出,更重要的是能提升你的编程能力和代码质量。建议读者结合实际项目练习,加深对这些概念的理解和应用。
记住:理论知识要与实践相结合,多写代码、多调试、多思考,才能真正掌握C/C++编程的精髓。