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