跳到主要内容C++ 核心知识点解析(九) | 极客日志C++
C++ 核心知识点解析(九)
本文解析了 C++ 十大核心知识点,包括源文件包含规范、深浅拷贝区别及实现、命名空间用法、友元机制、线程安全类设计(互斥锁/原子操作)、C 语言库调用、编译链接流程、auto 与 decltype 类型推断、虚函数多态原理以及 new/delete 配对规则。内容涵盖基础语法、内存管理、并发编程及底层机制,适合 C++ 开发者复习巩固。
1. C++ 是否可以 include 源文件?
可以 include 源文件。不过,尽管技术上可行,但这并不是一个推荐的做法,也不符合良好的编程习惯。
2. C++ 中什么是深拷贝?什么是浅拷贝?写一个标准的拷贝构造函数?
- **浅拷贝:**浅拷贝只是简单地复制对象的值,而不复制对象所拥有的资源或内存。也就是说,两个对象共享同一个资源或内存。当一个对象修改了该资源或内存,另一个对象也会受到影响。这种情况通常发生在默认的拷贝构造函数或赋值操作中。
- **深拷贝:**深拷贝不仅复制对象的值,还会新分配内存并复制对象所拥有的资源。这样两个对象之间就不会共享同一个资源或内存,修改其中一个对象的资源或内存不会影响到另一个对象。
举个例子,一个标准的深拷贝构造函数可以这样写:
#include <cstring>
#include <iostream>
class MyClass {
private:
char* data;
public:
MyClass(const char* inputData) {
data = new char[std::strlen(inputData) + 1];
std::strcpy(data, inputData);
}
MyClass(const MyClass& other) {
data = new char[std::strlen(other.data) + 1];
std::strcpy(data, other.data);
}
~MyClass() {
delete[] data;
}
void printData() {
std::cout << data << std::endl;
}
};
int main() {
MyClass ;
MyClass obj2 = obj1;
obj();
obj();
;
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown 转 HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
- HTML 转 Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
obj1
("Hello")
1.
printData
2.
printData
return
0
在这个例子中,MyClass类有一个指向字符数组的指针data。拷贝构造函数对另一个对象进行深拷贝,即为data分配新的内存并复制字符串,因此两个对象各自独立地拥有自己的数据。
3. C++ 中命名空间有什么作用?如何使用?
命名空间(namespace)主要用于解决名字冲突问题。当项目规模较大,包含很多函数、类、变量的时候,很容易出现名字相同的情况,这时候命名空间就显得特别重要。
namespace MyNamespace {
int myVar;
void myFunc() {
}
}
你可以通过命名空间的名称来访问其中的成员,比如 MyNamespace::myVar和MyNamespace::myFunc()。
4. C++ 中友元类和友元函数有什么作用?
友元关系是一种单向的访问权限,并不会破坏封装性,同时也不会牵涉到类之间的继承关系。友元的使用在以下情况下特别有用:
**1. 友元函数:**允许一个函数访问某个类的私有成员和保护成员。
class MyClass {
private:
int privateMember;
public:
MyClass() : privateMember(0) {}
friend void friendFunction(MyClass &obj);
};
void friendFunction(MyClass &obj) {
obj.privateMember = 10;
}
**2. 友元类:**允许另一个类访问某个类的私有成员和保护成员。
class B;
class A {
private:
int privateMember;
public:
A() : privateMember(0) {}
friend class B;
};
class B {
public:
void accessA(A &obj) {
obj.privateMember = 20;
}
};
- 封装是面向对象编程的基本原则之一,它将数据和操作数据的方法绑定到一起,防止外部代码直接访问对象的内部状态。友元的引入让类在需要的时候能够部分地开放它的内部状态,通常不会滥用。
- 友元函数和友元类提供了一种在不破坏封装性的条件下,安全访问私有成员的方式。
- 如果友元机制的使用本质上意味着违反封装性或设计初衷,那么可能需要重新考量类的设计。
- 你可以选择通过公开接口提供访问权限 (如 getter/setter 方法),或利用继承、多态等其他 OOP 特性来实现同样的目的。
- 使用友元可能会增加代码的复杂度,因为它打破了类的封装性,代码的维护变得相对困难。所以,在维护代码时,需要非常小心,确保友元使用的合理性和必要性。
友元是一种方便但需要慎用的工具,合理使用能够简化代码,但滥用则会破坏类的封装性,增加代码维护的难度。建议在实际编程中能够权衡利弊,合理利用这一机制。
5. C++ 如何设计一个线程安全的类?
- 使用互斥锁 (mutex) 保护共享资源。
- 部分逻辑可以使用无锁编程,原子变量控制。
- 使用线程消息队列形式,保证此类里的所有操作任务在一个队列里,都在一个线程内调度,自然而然就解决了多线程问题。
要设计一个线程安全的类,通常情况下,我们会使用互斥锁 (mutex) 来保护共享资源,确保在任何时刻只有一个线程可以访问修改这些资源。
下面是一个简单示例,展示了如何使用 std::mutex 来实现一个线程安全的类:
#include <iostream>
#include <thread>
#include <mutex>
class SafeCounter {
public:
SafeCounter() : value(0) {}
void increment() {
std::lock_guard<std::mutex> lock(mutex_);
++value;
}
int getValue() {
std::lock_guard<std::mutex> lock(mutex_);
return value;
}
private:
int value;
std::mutex mutex_;
};
int main() {
SafeCounter counter;
auto increment_func = [&counter]() {
for (int i = 0; i < 100; ++i) {
counter.increment();
}
};
std::thread t1(increment_func);
std::thread t2(increment_func);
t1.join();
t2.join();
std::cout << "Final value: " << counter.getValue() << std::endl;
return 0;
}
std::mutex 用于保护共享数据的访问。
std::lock_guard 是一个 RAII 类型的锁机制,用来确保在作用域结束时自动释放锁。
increment方法和getValue方法都使用锁保护共享数据。
有时我们需要实现的场景是多线程可以同时读数据,但写数据时需要独占锁。这可以使用 std::shared_mutex(C++17 引入) 来实现。std::shared_lock允许多个线程同时获取读锁,而std::unique_lock则用于写锁。
对于一些简单的整型操作,可以使用std::atomic来代替互斥锁。std::atomic提供了高效的原子操作,避免了锁的开销。
#include <atomic>
class AtomicCounter {
public:
AtomicCounter() : value(0) {}
void increment() {
value.fetch_add(1, std::memory_order_relaxed);
}
int getValue() {
return value.load(std::memory_order_relaxed);
}
private:
std::atomic<int> value;
};
当多个线程需要协调工作时,可以使用条件变量来等待特定条件满足后再进行操作。
6. C++ 如何调用 C 语言的库?
可以使用 extern"c" 来告诉编译器按照 C 语言的链接方式处理某些代码:
- 在 C++ 代码中包含 C 语言头文件时,用
extern"c"进行声明,比如:
extern "c" {
#include "your_c_library.h"
}
- 需要在链接阶段确保 C++ 项目和 C 语言库都被正确链接。可通过编写合适的 CMakeLists.txt 或 Makefile 来实现。
- 也可以不使用 extern"c",源文件后缀名改为.c 也行。
7. 介绍一下 C++ 程序从编写到可执行的整个过程?
- **编写代码:**编写 C++ 源代码,保存为
.cpp、.cc、.h文件。
- **预处理:**预处理器根据源代码中的预处理指令 (如
#include替换、#define替换等) 对代码进行处理,生成纯净的源代码。
- **编译:**编译器 (如 g++ 或 clang++) 将预处理后的源代码翻译成汇编代码。
- **汇编:**汇编器 (如
as) 将汇编代码转换成机器码,生成目标文件 (.o文件)。
- **链接:**链接器 (如
ld) 将多个目标文件和库文件链接在一起,生成最终的可执行文件。
8. 什么是 C++ 中的 auto 和 decltype?
两者都是 C++11 引入的新特性,主要用于类型推断。
1. auto关键字:用于自动推断变量的类型。编译器会根据变量的初始化表达式来推导变量的类型,这样开发者就不需要显式地声明类型。
auto x = 10;
auto y = 3.14;
auto str = "Hello, world!";
2. decltype关键字:用于推断表达式的类型。它会返回一个表达式所对应的类型信息,而不进行计算。这个在模板编程中特别有用。
int a = 10;
deprecated(a) b = 20;
deprecated(a + 10.0) c = 30.0;
9. 请介绍 C++ 多态的实现原理?
回答多态的实现原理,主要可以围绕在虚函数、虚函数表和虚函数表指针方向上。
多态通过虚函数实现。通过虚函数,子类可以重写父类的方法,当通过基类指针或引用调用时,会根据对象的实际类型调用对应的函数实现。
而这更深层次的原理,是通过虚表 (vtable) 和虚表指针 (vptr) 机制实现的。虚表是一个函数指针数组,包含了该类所有虚函数的地址,而虚表指针存储在对象实例中,指向属于该对象的虚表。
1. 虚函数和重写:在基类中使用关键字 virtual 声明虚函数后,在子类中可以重写这个函数。
class Base {
public:
virtual void show() {
std::cout << "Base show" << std::endl;
}
};
class Derived : public Base {
public:
void show() override {
std::cout << "Derived show" << std::endl;
}
};
2. 虚表 (vtable):每个包含虚函数的类都会有一个虚表 (vtable),这个虚表在编译时生成。它包含了该类所有虚函数的指针。对于每个类 (而不是每个对象),编译器会创建一个唯一的虚表。
3. 虚表指针 (vptr):每个包含虚函数的对象实例会有一个隐藏的虚表指针 (vptr),它在对象创建时自动初始化,指向该类的虚表。不同类型的对象,其虚表指针会指向不同的虚表。例如,上述示例中,Base 和 Derived 对象的虚表指针分别指向它们各自的虚表。
4. 多态的调用机制:当通过基类指针或引用调用虚函数时,程序会通过该指针或引用找到对应的对象,然后通过虚表指针找到正确的虚表中的函数地址,最终调用适当的函数实现,这样程序能够在运行时决定调用哪一个函数实现。
void demonstratePolymorphism(Base &obj) {
obj.show();
}
int main() {
Base b;
Derived d;
demonstratePolymorphism(b);
demonstratePolymorphism(d);
return 0;
}
10. C++ 中为什么 new[] 和 delete[] 一定要配对使用?
一定要配对使用,如果不正确地配对使用 new[]和 delete[],会导致:
- **内存泄漏:**如果用
new[]分配内存但用 delete(不带中括号) 来释放,数组中的每个对象的析构函数不会被调用,导致内存泄漏。
- **运行时错误:**如果用
new分配内存,但却用delete[]来释放,可能导致未定义行为。
具体来说,当用 new[]分配一块连续的内存时,编译器不仅管理实际数据的存储位置,还存储了数组的大小信息,以便在调用 delete[]时能够正确释放这块内存。而且,delete[]会负责调用数组中每个对象的析构函数,再释放整个数组的内存。