一、C++入门基础
1.1、函数重载
函数重载允许在同一作用域内定义多个同名函数,只要这个函数的参数列表(即参数的数量,类型或者顺序不同)
如何支持:程序经过编译后,编译器会对程序中的函数按一定规则进行重新命名加以修饰。
1.2、引用和指针
引用是对象的别名,一旦初始化之后就不能更改,且引用必须在创建时就初始化。引用更简单,更安全,不能为空。
指针是一个变量,用来存储另一个对象的地址,指针可以在运行时更改指向。指针更灵活,可以为空,可以重新赋值,支持指针运算,但是要避免悬挂指针和内存泄漏。
1.3、内联和宏
宏的缺点:调式困难,缺乏类型安全,维护困难,代码可读性差,灵活性差。
inline(内联)的要求:适用于频繁调用短小的函数。
1.4、nullptr的意义
NULL在底层定义时被定义为0
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
因此存在一定的缺点:
类型不安全:
NULL
被定义为0
时,它是一个整数常量。这可能导致指针与整数混淆,特别是在重载函数的选择中。- 如果
NULL
被定义为((void*)0)
,在C++中会引起类型不匹配问题,因为void*
不能隐式转换为其他类型的指针。
重载决策问题:
- 使用
NULL
时,可能会选择错误的重载版本,因为0
可以匹配多个重载函数的参数类型。
nullptr
在C++中提供了一个类型安全且表达明确的方式来表示空指针,克服了 #define NULL 0
所带来的类型不安全和重载决策问题。因此,在现代C++代码中,推荐使用 nullptr
代替传统的 NULL
。
二、类和对象
2.1、面向对象和面向过程的区别
面向过程编程适用于小型程序,强调函数和逻辑流程的顺序执行,简单直接但是数据保护性差。
面向对象编程适用于大型程序,强调通过对象和类组织代码,提高代码的模块化,复用性和可维护性,同时提供更好的数据保护。
2.2、class和struct的对比
相似:
都可以定义数据成员和成员函数,二者都支持继承,允许一个类或者一个结构体,从另外一个类或者结构体派生。都可以通过访问控制符来控制成员的访问权限。
区别:
1、class 的默认访问控制符是 private,struct的默认访问控制符是public。
2、struct 更多用于表示简单的数据结构,没有复杂的成员函数;而class更多用于表示具有复杂行为的封装数据的对象。
3、在继承中,class的默认继承方式是private,struct 的默认继承方式是public。
2.3、关于this指针
this指针存在哪里?
this指针是一个隐式参数,传递给非静态成员函数,指向调用该成员函数的对象本身。this指针的内存中的存储位置取决于具体的编译器实现和硬件架构,**但是通常是存储在寄存器和堆栈中。**存储在寄存器中可以加快访问速度,减少内存操作,提高程序的性能。
this指针可以为空吗?
在C++中this 指针永远不会为空指针,this指针指向调用函数的对象实例,因此只有存在对象实例的情况下,this指针才会存在。静态成员函数没有this指针,因为他们不是通过对象实例化调用的,而是通过类本身调用。
2.4、关于八个默认成员函数
1、八个默认成员函数
构造函数,析构函数,拷贝构造函数,赋值函数,移动构造函数,移动赋值函数。
2、什么情况下要自己写?
一般构造都要自己写
深拷贝的类,一般都要自己写析构、拷贝构造、赋值。
深拷贝的类,也需要实现移动构造和移动赋值
3、默认生成这些函数行为是什么?
当类中没有用户自定义默认成员函数,对于自定义类型编译器会生成默认成员函数;
对于内置类型编译器不会生成默认成员函数,因为他们没有这些概念。内置类型可以直接进行赋值和拷贝。
4、初始化列表的特性
高效性:初始化列表直接构造成员变量,而不是先调用默认构造函数再赋值,避免了不必要的临时对象创建和赋值操作。
**常量成员:**常量成员(const)必须通过初始化列表初始化,因为他们只能被赋值一次。
**引用成员:**引用数据成员必须通过初始化列表初始化,因为引用必须在定义时绑定到有效的对象。
**基类构造:**派生类的构造函数通过初始化列表调用基类的构造函数,确保基类部分在派生类构造之前正确初始化。
**成员初始化顺序:**成员变量的初始化顺序与他们在类中声明的顺序相同,与初始化列表的顺序无关。
5、必须在初始化列表中初始化的成员
成员变量(const成员),引用成员,基类,无默认构造函数的成员。
使用初始化列表不仅可以提高性能,还能确保在编译时对必须初始化的成员进行正确的初始化。
2.5、运算符的重载
. :: sizeof ?: . 注意以上5个运算符不能重载。*
运算符重载和函数重载的区别:
运算符重载和函数重载都是C++中多态性的实现方式,但是适用于不同的场景,运算符重载使自定义类型的对象可以使用类似于内置类型的运算符,函数重载通过不同的参数列表提供多种实现。
运算符重载的意义:
运算符重载的主要意义在于提高代码的可读性、简洁性和一致性,使自定义类型的对象操作更加自然和直观,同时与标准库容器和算法更好地协同工作。通过运算符重载,可以定义符合预期的类型操作,从而增强类型安全性和代码的可维护性。
2.6、static
static关键字可以用来定义静态成员变量和静态成员函数。静态成员与类的实例无关,它们属于整个类,而不是类的实例。
静态成员变量是类的所有实例共享的变量,它不属于任何特定的对象,而是属于整个类。只有一个静态成员变量的实例存在,并且在程序的生命周期内保持一致。
静态成员函数属于类而不是对象的特定实例。它们没有this
指针,因此不能访问非静态成员变量和非静态成员函数。通常用来执行与类相关但不依赖于特定对象实例的操作。
三、内存管理
3.1、malloc、calloc、realloc的区别。
**在初始化方面:**malloc不初始化分配的内存,calloc初始化分配的内存,所有的字节都被初始化为零,realloc调整内存块大小,常用于扩容,扩容部分不初始化。
**在参数方面:**malloc是一个参数,表示要分配的内存块大小;calloc两个参数,表示要分配的元素个数和每个元素的大小;realloc两个参数,一个指向元内存块的指针,一个是新的内存块大小。
**在用法方面:**malloc用于分配指定大小的未初始化内存块;calloc用于分配指定数量和大小的初始化为零的内存块;realloc用于调整已分配的内存块大小,可能会移动内存块。
3.2、new/delete和malloc/free的区别 (重点)
new
/delete
:更适合C++对象的内存管理,自动调用构造函数和析构函数,具有类型安全性,在内存分配失败时抛出异常。
malloc
/free
:适用于基本数据类型和需要手动控制内存分配的场景,不调用构造函数和析构函数,不具有类型安全性,在内存分配失败时返回 NULL
。
在C++编程中,通常推荐使用 new
和 delete
来管理对象的动态内存,以充分利用C++的面向对象特性和异常处理机制。而对于基本数据类型和特定的性能优化需求,可以考虑使用 malloc
和 free
。
new和new []的底层实现原理:operator new + 构造函数
3.3、内存泄漏的危害
性能下降:随着内存泄漏的累积,可用内存逐渐减少,导致系统性能下降,响应时间变长。
程序崩溃:当内存泄漏严重时,可能会耗尽系统的可用内存,导致程序崩溃或操作系统变得不稳定。
资源浪费:未释放的内存占用了系统资源,无法被其他程序或系统使用,导致资源浪费。
难以调试:内存泄漏通常是累积性的,可能不会立即显现出来,调试和排查内存泄漏问题通常比较困难。
3.4、如何避免内存泄漏
及时释放内存:确保每个malloc
、calloc
或new
分配的内存都能在适当的时候使用free
或delete
释放。
智能指针:在C++中使用智能指针(如std::unique_ptr
和std::shared_ptr
)管理动态内存,智能指针自动管理内存释放,减少手动管理的复杂性和错误。
RAII(Resource Acquisition Is Initialization):利用RAII模式,将资源的获取和释放绑定到对象的生命周期中,确保资源能够在对象销毁时自动释放。
代码审查和静态分析工具:定期进行代码审查,使用静态分析工具(如Valgrind、Clang Static Analyzer)检测潜在的内存泄漏问题。
避免循环引用:在使用智能指针时,尤其是std::shared_ptr
,要注意避免循环引用问题,可以使用std::weak_ptr
打破循环引用。
四、模板
4.1模板的原理
通过定义结构和可变部分的结合,用来减少重复性工作,提高效率和一致性。
模板的声明和定义不支持分离写到.h和.cpp文件中,最佳解决方案是放到同一个文件中。
五、继承
5.1、继承的意义
通过提高代码的重用性,可维护性和灵活性为开发者提供了一种高效的编程方式。
5.2、什么是继承
继承是面向对象编程中的一个核心概念,指的是一个类从另一个类获得属性和方法的机制,允许子类在不重新编写相同代码的情况下重用父类的代码,同时还可以扩展或者修改这些属性和方法。
5.3、隐藏
函数名相同且不构成重写的就是隐藏。
子类的默认成员函数是要复用父类的
5.4、多继承
避免使用菱形继承
在多继承中可能会导致数据冗余和二义性问题。
虚继承可以解决数据冗余和二义性,原理:通过在继承声明中指定基类为虚基类,编译器会确保在继承链中只存在一个虚基类的实例,这样当子类通过不同路径继承同一个虚基类时,虚基类的成员不会重复出现,从而避免数据冗余和二义性。
5.5、继承和组合
继承:is-a 子类是父类的一种特例
组合: has- a 一个类包含另一个类的实力作为其属性
组合耦合度低,继承耦合度高,应该优先使用组合
六、多态
6.1、什么是多态
多态是相同接口在不同对象中表现出不同的行为,允许对象通过同样的接口来调用不同的实现,从而提高代码的灵活性和可扩展性。
静态的多态(编译时多态)–》函数重载或者运算符重载,编译时是参数匹配和函数名修饰规则
动态多态(运行时多态)–》通过方法重写实现,跟面向对象有关运行时多态依赖于继承和接口,通过父类引用指向子类对象来实现。
虚函数重写,虚函数子类中虚函数可以不加virtual,函数名相同,参数列表相同,返回值类型相同,协变除外
父类指针或者引用去调用虚函数,
在面向对象编程中,指向父类的指针或引用调用父类的虚函数,而指向子类的指针或引用则调用子类的虚函数,这种机制称为动态绑定或运行时多态。
6.2、为什么析构函数要是虚函数
确保派生类对象在销毁时,所有层次的析构函数都被正确调用,从而避免资源泄露和未定义行为,确保程序的安全性和正确性。
6.3、纯虚函数
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。
间接强制子类重写,因为不重写子类依旧是抽象类。
6.4、重载/重写(覆盖)/隐藏(重定义)的区别
重载(Overloading):同一个类中定义多个同名函数,但参数列表不同。
重写(Overriding):派生类中重新定义基类中的虚函数,参数列表和返回类型必须相同,实现多态性。
隐藏(Hiding):派生类中定义了一个与基类同名但不同参数的函数,或基类函数不是虚函数,从而隐藏了基类的函数。
6.5、多态的原理
虚函数重写以后,父类对象虚表指针指向的虚函数表存的是父类的虚函数,虚函数重写以后子类对象虚表指针指向的虚函数表存的是子类重写的虚函数。
父类指针或引用调用虚函数—》》去指向对象虚表中查找对应的虚函数进行调用,最终达到了指向谁,调用谁
七、C++11
7.1、范围for
int arr[10];
for(auto e : arr)
{
cout << e << endl;
}
7.2、右值引用
//左值引用
int x = 10;
int& ref = x; // ref是x的左值引用
ref = 20; // 修改ref也会修改x
void increment(int& a) {
a++;
}
int main() {
int num = 5;
increment(num); // 传递左值引用,num变为6
return 0;
}
//右值引用
int&& rvalueRef = 10; // rvalueRef是绑定字面值10的右值引用
void printValue(int&& a) {
std::cout << "Value: " << a << std::endl;
}
int main() {
printValue(100); // 传递右值引用
return 0;
}
左值引用(lvalue reference):用于绑定左值,定义时使用&。适用于需要传递和修改左值的情况。
右值引用(rvalue reference):用于绑定右值,定义时使用&&。适用于需要传递和修改右值、实现移动语义和优化性能的情况。
**使用场景:**深拷贝的类中做参数来提高效率,传值返回的优化,
7.3、移动构造和移动赋值
移动构造函数:用于创建一个新对象并转移资源,避免不必要的资源复制。定义时使用右值引用参数,通常带有noexcept
。
移动赋值运算符:用于将资源从一个右值对象转移到一个已有对象,避免资源复制。定义时使用右值引用参数,并返回当前对象的引用。
这两者在结合使用时,可以显著提高程序的性能,特别是在处理大量资源(如动态内存、文件句柄等)的情况下。移动语义使得对象的所有权可以在不同对象之间高效地转移,而不需要进行昂贵的深拷贝操作。
7.4、完美转发
完美转发主要依赖于模板和右值引用,通过使用std::forward
和std::move
来实现。
**原理:**完美转发的核心在于如何区分传递的参数是左值还是右值,并根据参数类型选择合适的转发方式。std::forward
的实现是基于模板参数推导的条件编译:
- 如果参数是左值,
std::forward
会返回左值引用。 - 如果参数是右值,
std::forward
会返回右值引用。
用途:
泛型编程:在模板函数中使用完美转发,可以确保参数的完美传递。
构造函数和工厂函数:在构造函数或工厂函数中使用完美转发,可以避免不必要的拷贝操作。
资源管理:在资源管理类中使用完美转发,可以实现高效的资源转移。
简化实现
template <typename T>
T&& forward(typename std::remove_reference<T>::type& arg) {
return static_cast<T&&>(arg);
}
7.5、push和emplace系列的区别
push
系列:需要传递一个已经构造好的对象,可能会有额外的拷贝或移动操作。
emplace
系列:直接在容器中构造对象,通常更高效,避免了不必要的拷贝或移动操作。
7.6、lambda表达式
Lambda表达式提供了一种简洁且高效的方法来定义内联函数,特别适用于需要传递简单函数对象的场景。通过捕获外部变量和自动推导返回类型,Lambda表达式使得代码更加灵活和易读。在现代C++编程中,Lambda表达式已成为标准库算法和事件处理等操作中不可或缺的一部分。
#include <iostream>
int main() {
int factor = 2;
// 捕获外部变量factor
auto multiply = [factor](int value) {
return value * factor;
};
std::cout << "5 * 2 = " << multiply(5) << std::endl; // 输出10
return 0;
}
八、异常
异常处理在提供清晰的错误处理逻辑和自动资源管理方面有显著优点,但也带来了性能开销和控制流复杂性等问题。在实际应用中,应该根据具体情况合理使用异常,避免过度依赖,以达到最佳的代码质量和性能。
九、智能指针
9.1、RAll
RAII是一种非常有效的资源管理方法,通过将资源的生命周期绑定到对象的生命周期上,可以确保资源在整个生命周期内都能被正确地管理。
使用RAII可以提高代码的健壮性、可读性和可维护性,使资源管理变得更加简单和安全。在现代C++编程中,RAII已成为管理资源的标准方法之一。可以有效的避免资源泄露并简化资源管理的代码。
RAII的核心理念
- 资源获取即初始化:在对象的构造函数中获取资源,在析构函数中释放资源。
- 对象生命周期管理资源:当对象的生命周期结束时,资源自动释放。
RAII的优点
- 自动化资源管理:无需手动释放资源,减少了代码中的错误。
- 异常安全:即使在异常发生时,析构函数仍然会被调用,确保资源被正确释放。
- 简洁代码:减少了显式的资源管理代码,使代码更加简洁和易读。
9.2、auto_ptr/uniqur_ptr/shared_ptr/weak_ptr特点是什么?解决了什么问题?
auto_ptr
:已弃用,独占所有权,赋值会转移所有权,不适合容器使用。自动释放资源,当他管理的对象销毁时就会自动释放资源。所有权转移,拷贝语义不安全
unique_ptr
:独占所有权,支持移动语义,轻量高效,适用于独占资源管理。
shared_ptr
:共享所有权,引用计数管理生命周期,适用于共享资源管理。
weak_ptr
:不控制资源生命周期,避免循环引用,不增加引用计数,通过lock方法安全方位资源。常与shared_ptr
结合使用。
解决手动内存管理带来的问题,尤其是资源泄漏和悬挂指针问题。
9.3、模拟实现
#include <iostream>
template <typename T>
class UniquePtr {
public:
explicit UniquePtr(T* ptr = nullptr) : ptr_(ptr) {}
// 禁用拷贝构造和拷贝赋值
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
// 移动构造和移动赋值
UniquePtr(UniquePtr&& other) noexcept : ptr_(other.ptr_) {
other.ptr_ = nullptr;
}
UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) {
delete ptr_;
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
return *this;
}
~UniquePtr() {
delete ptr_;
}
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
T* get() const { return ptr_; }
T* release() {
T* temp = ptr_;
ptr_ = nullptr;
return temp;
}
void reset(T* ptr = nullptr) {
delete ptr_;
ptr_ = ptr;
}
private:
T* ptr_;
};
int main() {
UniquePtr<int> p1(new int(10));
UniquePtr<int> p2 = std::move(p1); // p1失去所有权,p2获得所有权
if (!p1) {
std::cout << "p1 is null\n";
}
if (p2) {
std::cout << "p2: " << *p2 << std::endl;
}
return 0;
}
#include <iostream>
#include <atomic>
template <typename T>
class SharedPtr;
template <typename T>
class WeakPtr;
template <typename T>
class ControlBlock {
public:
ControlBlock(T* ptr) : ptr_(ptr), ref_count(1), weak_count(0) {}
void addRef() { ++ref_count; }
void releaseRef() {
if (--ref_count == 0) {
delete ptr_;
if (weak_count == 0) {
delete this;
}
}
}
void addWeak() { ++weak_count; }
void releaseWeak() {
if (--weak_count == 0 && ref_count == 0) {
delete this;
}
}
T* getPtr() const { return ptr_; }
int getRefCount() const { return ref_count; }
private:
T* ptr_;
std::atomic<int> ref_count;
std::atomic<int> weak_count;
};
template <typename T>
class SharedPtr {
public:
explicit SharedPtr(T* ptr = nullptr) : cb_(new ControlBlock<T>(ptr)) {}
SharedPtr(const SharedPtr& other) : cb_(other.cb_) {
cb_->addRef();
}
SharedPtr& operator=(const SharedPtr& other) {
if (this != &other) {
if (cb_) {
cb_->releaseRef();
}
cb_ = other.cb_;
if (cb_) {
cb_->addRef();
}
}
return *this;
}
~SharedPtr() {
if (cb_) {
cb_->releaseRef();
}
}
T& operator*() const { return *cb_->getPtr(); }
T* operator->() const { return cb_->getPtr(); }
T* get() const { return cb_ ? cb_->getPtr() : nullptr; }
int use_count() const { return cb_ ? cb_->getRefCount() : 0; }
private:
ControlBlock<T>* cb_;
friend class WeakPtr<T>;
};
template <typename T>
class WeakPtr {
public:
WeakPtr() : cb_(nullptr) {}
WeakPtr(const SharedPtr<T>& sp) : cb_(sp.cb_) {
if (cb_) {
cb_->addWeak();
}
}
WeakPtr(const WeakPtr& other) : cb_(other.cb_) {
if (cb_) {
cb_->addWeak();
}
}
WeakPtr& operator=(const WeakPtr& other) {
if (this != &other) {
if (cb_) {
cb_->releaseWeak();
}
cb_ = other.cb_;
if (cb_) {
cb_->addWeak();
}
}
return *this;
}
~WeakPtr() {
if (cb_) {
cb_->releaseWeak();
}
}
SharedPtr<T> lock() const {
return (cb_ && cb_->getRefCount() > 0) ? SharedPtr<T>(*this) : SharedPtr<T>();
}
private:
ControlBlock<T>* cb_;
};
int main() {
SharedPtr<int> sp1(new int(10));
WeakPtr<int> wp1(sp1);
std::cout << "sp1 use count: " << sp1.use_count() << std::endl;
if (auto sp2 = wp1.lock()) {
std::cout << "sp2: " << *sp2 << std::endl;
} else {
std::cout << "sp2 is null\n";
}
sp1 = nullptr;
if (auto sp2 = wp1.lock()) {
std::cout << "sp2: " << *sp2 << std::endl;
} else {
std::cout << "sp2 is null\n";
}
return 0;
}
UniquePtr
:
UniquePtr
类管理独占的资源。- 禁用拷贝构造和赋值,只允许移动构造和赋值。
- 提供
get
、release
和reset
方法来操作内部指针。
SharedPtr
:
SharedPtr
类管理共享资源。- 使用
ControlBlock
来管理资源和引用计数。 - 提供
get
和use_count
方法来访问内部指针和引用计数。
WeakPtr
:
WeakPtr
类用于打破循环引用。- 不影响引用计数,但可以检查资源是否仍然存在。
- 提供
lock
方法来获得一个SharedPtr
。
9.4、什么是循环引用?怎么解决?
循环引用(circular reference)是在两个或多个对象之间互相引用,导致这些对象无法被正确释放的情况。循环引用会造成内存泄漏,因为引用计数无法降到零,导致资源无法被回收。
解决:解决循环引用的常用方法是使用weak_ptr
,它不增加引用计数,从而打破了循环引用。通过正确使用weak_ptr
,可以确保资源在不再使用时被正确释放,避免内存泄漏。
十、类型转换
四种转换是什么?什么场景下使用
static_cast
:适用于编译时已知且类型安全的转换。
dynamic_cast
:用于多态类型的安全向下转换,依赖运行时类型检查。
const_cast
:用于修改对象的 const
或 volatile
属性。
reinterpret_cast
:用于进行低级别的、潜在不安全的类型转换。
十一、IO流
库的意义:面向对象,支持自定义类型流插入和流提取。