文章目录
C++
1. C++是解释型语言还是编译型语言
是编译型。解释型是边编译边执行,如Python。编译型语言是把源程序全部编译成二进制的可运行程序后再运行 。
注:java程序不是直接编译成机器语言,而是编译成字节码,然后用解释方式执行字节码。
2. C++是面向对象语言还是面向过程语言
是面向对象。面向对象编程(OOP)的特点包括:
- 封装:将数据和操作这些数据的方法组合在一起。
- 继承:允许新创建的类(子类)继承现有类(父类)的属性和方法。
- 多态:允许不同类的对象对同一消息做出响应,但具体的行为会根据对象的实际类型而有所不同。
面向过程编程的特点包括:
- 将程序分解为一系列的过程或函数,每个过程或函数执行特定的任务。
- 强调过程和函数的调用,而不是对象和类。
3. C++和C的关系
C++是在C的基础上发展而来的,但是两者有着本质区别。C 是一种过程式编程语言,而C++是面向对象的。
4.struct 和 class 区别
**最主要的区别:**struct默认成员访问权限为public,而class默认成员访问权限为private
5.构造函数
- 默认构造函数:如果没有定义任何构造函数,编译器会生成一个默认构造函数,它不初始化任何成员变量。
- 带参数的构造函数:可以定义一个或多个带参数的构造函数来初始化对象。
- 拷贝构造函数:用于创建一个对象的副本。
- 移动构造函数:用于创建一个对象的移动语义的副本。
- 委托构造函数:构造函数可以调用同一类中的另一个构造函数。
#include <bits/stdc++.h>
using namespace std;
class Person {
private:
string name;
int age;
vector<string> hobbies;
public:
// 默认构造函数
Person() : name("Unknown"), age(0), hobbies() {}
// 带参数的构造函数
Person(const string& name, int age) : name(name), age(age) {}
// 带参数和初始值列表的构造函数
Person(const string& name, int age, const vector<string>& hobbies)
: name(name), age(age), hobbies(hobbies) {}
// 拷贝构造函数
Person(const Person& other) : name(other.name), age(other.age), hobbies(other.hobbies) {}
// 成员函数
void printInfo() const {
cout << "Name: " << name << ", Age: " << age << endl;
cout << "Hobbies: ";
for (const auto& hobby : hobbies) {
cout << hobby << " ";
}
cout << endl;
}
};
int main() {
// 使用默认构造函数
Person person1;
person1.printInfo();
// 使用带参数的构造函数
Person person2("Alice", 30);
person2.printInfo();
// 使用带参数和初始值列表的构造函数
vector<string> aliceHobbies = {"reading", "swimming"};
Person person3("Alice", 30, aliceHobbies);
person3.printInfo();
// 使用拷贝构造函数
Person person4 = person2;
person4.printInfo();
return 0;
}
// 输出:
// Name: Unknown, Age: 0
// Hobbies:
// Name: Alice, Age: 30
// Hobbies:
// Name: Alice, Age: 30
// Hobbies: reading swimming
// Name: Alice, Age: 30
// Hobbies:
6. 虚函数
- 使用
virtual
关键字声明。 - 隐藏:如果派生类有一个与基类同名的函数,即使参数列表相同,基类中的函数并不自动成为虚函数。要实现多态性,基类中的函数必须被声明为虚函数。
- 调用:通过基类指针或引用调用虚函数时,将调用对象实际类型中的函数版本。
- 纯虚函数:如果基类中的虚函数没有实现,并且希望派生类必须提供自己的实现,可以将虚函数声明为纯虚函数,使用
= 0
。
- 构造函数不可以是虚函数
- 假设构造函数是虚函数,对象还没有实例化时无法通过vtable来调用,因为还没有分配内存空间
- 析构函数必须是虚函数
- 防止内存泄漏
- 虚函数通过虚函数表(virtual table,也称为vtable)实现。虚函数表是一个指向虚函数的指针数组,每个虚函数在数组中对应一个条目。当调用虚函数时,程序会通过对象的虚函数表找到对应的虚函数并调用它。
- 只有通过指向对象的指针或引用调用虚函数才会触发动态绑定(dynamicbinding),也就是根据对象的实际类型来调用函数。如果直接使用对象来调用虚函数,则会按照对象的静态类型来调用函数。
7.静态函数
- 静态函数有静态存储期,这意味着它们在程序的整个运行期间都存在,而不是在每次函数调用时创建和销毁
- 静态函数是与类相关联但不属于某个特定对象的函数。它们可以通过类名直接调用,而不需要创建类的实例。(类似于写在类里面的普通函数)
- 使用
static
关键字声明。
野指针&空指针
野指针:指向的内存地址无明确设置(未初始化或初始化为一个无效的内存)-》不安全
空指针:被显式设置为NULL或nullptr的指针,它不指向任何有效的内存地址-》安全
- 野指针不会直接引发错误,操作野指针指向的内存区域才会出问题
- 解引用空指针将导致异常
引用
引用(Reference)是一种特殊的类型,它是一个已经存在的对象的别名。所以引用本身不是对象,它没有自己的内存地址,且不能被重新赋值或指向nullptr。
- 声明:引用使用
&
符号声明,语法形如type& referenceName = existingObject;
。 - 初始化:引用在声明时必须被初始化,即必须同时指定它所引用的对象。
- 别名:引用作为原始对象的别名,对引用的操作实际上是对原始对象的操作。
- 左值:引用是左值,可以出现在需要左值的上下文中,如赋值的左侧。
- 常量引用:使用
const
关键字可以声明常量引用,这意味着不能通过这个引用修改对象的值。 - 引用的引用:C++不允许引用的引用,即一个引用不能绑定到另一个引用。
- 引用的生命周期:引用的生命周期与它所引用的对象相同,因此不存在悬挂引用(dangling reference)的问题。
非常量左值是不能引用右值的
- 指针和引用的区别?
- 指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。(是否可变)
- 指针本身在内存中占有内存空间,引用相当于变量的别名,本身不是对象,在内存中不占内存空间。(是否占内存)
- 指针可以为空,但是引用必须绑定对象。(是否可为空)
- 指针可以有多级,但是引用只能一级。(是否能为多级)
左值&右值
在C++中,左值(Lvalue)和右值(Rvalue)是表达类型的两种基本分类:
- 左值(Lvalue):指的是那些有明确内存地址的表达式,可以出现在赋值表达式的左侧或右侧。左值可以用于取地址,并且通常用于引用可变状态。
- 右值(Rvalue):指的是那些没有明确内存地址的表达式,如临时对象、被销毁值的引用、字面量等。右值通常表示瞬时或匿名对象,不能被赋值,只能被消耗(例如,用作函数参数或赋值操作的右侧)。
C++11引入了新的引用类型,专门用于处理这两种值:
-
左值引用(Lvalue Reference):
- 左值引用绑定到左值上,即具有持久存储的对象。
- 声明时使用
type&
的形式。
-
右值引用(Rvalue Reference):
- 右值引用是C++11引入的特性,用来绑定到右值上,允许“移动语义”(move semantics)和“完美转发”(perfect forwarding)。
- 声明时使用
type&&
的形式。
- 绑定对象:左值引用必须绑定到左值上,而右值引用必须绑定到右值上。
- 用途:左值引用用于常规引用,右值引用则主要用于实现资源的转移,例如移动构造函数和移动赋值操作符。
- 临时对象:左值引用不能绑定到临时对象上,而右值引用可以。
#include <iostream>
#include <utility> // For std::move
void process(int& i) {
std::cout << "process(int& i) - Lvalue Reference" << std::endl;
}
void process(int&& i) {
std::cout << "process(int&& i) - Rvalue Reference" << std::endl;
}
int main() {
int a = 5;
int b = 10;
// 左值引用绑定到左值上
process(a);
// 右值引用绑定到右值上
process(std::move(a)); // a被转换为右值
// 下面的代码将导致编译错误,因为左值引用不能绑定到右值上
// process(b + a);
return 0;
}
移动语义
移动语义(Move Semantics)是一种优化技术,用于高效地处理临时对象或右值引用。
主要通过以下三个特性实现:
-
移动构造函数:允许一个对象从另一个对象那里“移动”其资源,而不是进行拷贝。
-
移动赋值操作符:类似于移动构造函数,移动赋值操作符允许对象通过“移动”另一个对象的资源来更新自己的状态。
-
右值引用:允许显式地绑定到右值上,从而触发移动构造函数或移动赋值操作符。
移动构造函数
移动构造函数接受一个右值引用类型的参数,允许对象在构造时从另一个对象那里接管资源。移动构造函数的声明通常如下:
class MyClass {
public:
MyClass(MyClass&& other) { // 移动构造函数
// 接管资源,例如:
this->data = std::move(other.data);
// 将other置于有效但未定义状态
other.data = nullptr;
}
// ...
};
移动赋值操作符
移动赋值操作符接受一个右值引用类型的参数,允许对象通过接管另一个对象的资源来更新自己的状态:
class MyClass {
public:
MyClass& operator=(MyClass&& other) { // 移动赋值操作符
if (this != &other) {
// 释放当前对象的资源
delete this->data;
// 接管other的资源
this->data = other.data;
other.data = nullptr;
}
return *this;
}
// ...
};
右值引用和 std::move
右值引用允许我们将一个左值显式地转换为右值,从而触发移动构造函数或移动赋值操作符。std::move
是一个标准库函数模板,用于将左值转换为右值引用:
MyClass obj1;
MyClass obj2 = std::move(obj1); // obj1 被转换为右值,调用移动构造函数
在这个例子中,使用 std::move
将 obj1
转换为右值,从而触发 MyClass
的移动构造函数。
#include <iostream>
#include <vector>
class BigData {
public:
BigData(size_t size) : data(new int[size]) {
std::cout << "BigData created\n";
}
~BigData() {
delete[] data;
std::cout << "BigData destroyed\n";
}
BigData(const BigData&) = delete; // 禁止拷贝构造
BigData& operator=(const BigData&) = delete; // 禁止拷贝赋值
BigData(BigData&& other) noexcept : data(other.data) {
std::cout << "BigData moved\n";
other.data = nullptr;
}
BigData& operator=(BigData&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
private:
int* data;
};
BigData createBigData() {
return BigData(1024);
}
int main() {
BigData original = createBigData();
BigData moved = std::move(original);
// original 现在处于有效但未定义状态
}
move注意事项:
- 不改变类型:move不改变对象的类型,它只是将对象转换为右值引用,实际的类型仍然是左值。
- 不实际移动:move 本身不执行移动操作,它只是告诉编译器使用移动构造函数或移动赋值操作符,具体的行为取决于对象的实现。
内存管理
- 堆区:一般由程序员自动分配,如果程序员没有释放,程序结束时可能有OS回收。其分配类似于链表。
- 栈区:由编译器自动分配和释放,存放为运行函数分配的局部变量,函数参数,返回数据,返回地址等,其操作类似于数据结构总的栈。
- 全局区(静态区static):存放全局变量,静态变量,常量。结束后由系统释放。
- 常量区(文字常量区):存放常量字符串,程序结束后有系统释放。
- 代码区:存放函数体(类成员函数和全局区)的二进制代码。
new 和malloc 的区别
1)都可用来申请动态内存和释放内存,都是在堆(heap)上进行动态的内存操作。
2)malloc和free是c语言的标准库函数,new/delete是C++的运算符。
3)new会自动调用对象的构造函数,delete 会调用对象的析构函数, 而malloc返回的都是void指针。
4)对于非内部数据类型的对象而言,光用malloc和free无法满足动态对象的要求。
5)因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
栈和堆的区别
申请方式:栈是系统自动分配,堆是程序员主动申请。申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上。
栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的。
申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。
存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制。
内存对齐
- 自然对齐:数据按照其大小自然对齐,例如,一个
int
类型的变量通常会在地址为4的倍数的内存位置。 - 强制对齐:编译器或程序员通过特定的指令或属性来强制数据在特定的边界上对齐。
内存泄漏
原因:
- 丢失对已分配内存的引用:如果分配的内存没有被适当地引用或记录,当原始的引用超出作用域后,就无法释放这部分内存。
- 错误的内存释放:错误地释放同一块内存多次,或者释放未被分配的内存,可能会导致程序崩溃或未定义行为。
- 异常处理不当:在发生异常时,如果没有适当的清理机制,可能会导致分配的内存没有被释放。
- 循环引用:特别是在使用智能指针时,循环引用可能会导致引用计数无法归零,从而内存不会被释放。
- 资源未关闭或未释放:除了内存资源外,其他类型的资源(如文件句柄、网络连接等)如果未被正确关闭或释放,也可能导致资源泄漏。
解决方法:
- 使用智能指针:C++11引入的智能指针(如
std::unique_ptr
和std::shared_ptr
)可以自动管理内存,减少内存泄漏的风险。 - 内存泄漏检测工具:使用专门的内存泄漏检测工具(如 Valgrind、AddressSanitizer 等)可以帮助发现和定位内存泄漏。
- 代码审查:定期进行代码审查,确保内存分配和释放的逻辑正确无误。
- 异常安全:确保在发生异常时,所有分配的内存都能被适当地清理。
- 资源管理:使用 RAII(Resource Acquisition Is Initialization)原则,确保资源在作用域结束时自动释放。
- 单元测试:编写单元测试来检测内存使用情况,确保内存分配和释放的正确性。
智能指针
用于自动管理动态分配的内存,确保资源的生命周期得到合理控制,避免内存泄漏。
std::unique_ptr
- 独占所有权:
std::unique_ptr
给予绝对的独占权,即在任意时刻只能有一个std::unique_ptr
指向特定资源。 - 不可复制:不允许拷贝操作,但可以移动(move),移动后原指针将不拥有资源。
- 使用场景:适用于明确某个资源只需要一个所有者的情况。
std::shared_ptr
- 共享所有权:多个
std::shared_ptr
可以指向同一资源,通过引用计数机制管理资源的生命周期。 - 引用计数:内部维护一个计数器,每当有新的
std::shared_ptr
指向资源时,计数增加;每当一个std::shared_ptr
被销毁时,计数减少,计数为零时释放资源。 - 循环引用问题:如果
std::shared_ptr
之间形成循环引用,引用计数可能永远不会达到零,导致内存泄漏。此时可以使用std::weak_ptr
解决。 - 使用场景:适用于多个对象需要共享资源的场景。
std::weak_ptr
- 弱引用:
std::weak_ptr
是对std::shared_ptr
的一种补充,它不拥有资源,也不增加引用计数。 - 解决循环引用:常用于解决
std::shared_ptr
之间可能产生的循环引用问题。 - 访问资源:通过
lock
成员函数尝试获取一个std::shared_ptr
,如果引用计数为零,则返回空指针。 - 使用场景:适用于需要观察或访问由
std::shared_ptr
管理的资源,但又不想拥有资源的场景。
内联
可以将一个函数的代码直接嵌入到每个调用该函数的地方,而不是生成单独的函数调用代码。内联可以减少函数调用的开销,尤其是在函数体较小且调用频繁的情况下。
- 使用
inline
关键字声明内联函数 - 编译时决定:是否内联一个函数最终由编译器决定。
- 虚函数、递归函数、含有循环或较大函数通常不会被内联
- why? 使用虚函数时,编译器通常生成一个虚函数表(vtable),并在运行时通过这个表来解析函数调用。由于这个解析过程发生在运行时,编译器无法在编译时内联这些函数。
协程
协程(Coroutine)是一种程序组件,用于更高级的异步编程。协程允许执行过程在等待操作完成时挂起(suspend)和恢复(resume)。使用 co_await
、co_yield
和 co_return
等关键字声明
- 特点:
- 非抢占式:协程的执行是协作的,一个协程可以选择主动挂起,让出控制权给另一个协程。
- 轻量级:协程通常比线程更轻量级,创建和切换的开销小。
- 用户态调度:协程的调度通常由程序控制,而不是由操作系统内核管理。
- 适用于I/O密集型任务:协程非常适合处理I/O密集型任务,如网络请求、文件读写等,因为它们可以在等待I/O操作完成时挂起,从而提高资源利用率。
-
与线程的区别:
- 调度方式:线程由操作系统内核进行抢占式调度,而协程由用户态代码进行非抢占式调度。
- 资源消耗:线程是重量级的操作,创建和切换开销较大,协程则更轻量。
- 使用场景:线程适用于计算密集型任务,协程适合I/O密集型任务。
-
示例代码(C++20):
#include <iostream>
#include <coroutine>
#include <chrono>
std::coroutine<void, int> my_coroutine(int value) {
for (int i = 0; i < 3; ++i) {
std::cout << "Coroutine: " << value << std::endl;
value += 10;
co_await std::suspend_always{}; // 模拟异步等待
}
}
int main() {
auto coro = my_coroutine(0);
while (coro) {
coro.resume(); // 手动恢复执行
}
return 0;
}
constexpr
constexpr
(常量表达式)是C++11中引入的一个关键字,用于声明一个在编译时可以求值的常量表达式。
override
override用在virtual前,声明后虚函数必须需要重写。
类型转换
在C++中,有四种主要的类型转换方式,分别是:
-
静态类型转换(Static Cast):
- 使用关键字
static_cast<type>(expression)
进行转换。 - 用于非多态类型的安全转换,例如基本数据类型的转换。
- 可以在相关类型之间进行向上转型(upcasting)和向下转型(downcasting),但不会进行运行时检查。
示例:
int i = 10; double d = static_cast<double>(i); // 基本数据类型转换
- 使用关键字
-
动态类型转换(Dynamic Cast):
- 使用关键字
dynamic_cast<type>(expression)
进行转换。 - 用于处理多态性,只能在具有虚函数的对象之间进行转换。
- 向下转型时会在运行时检查转换的安全性,并返回正确的对象;如果转换失败,将返回空指针。
示例:
class Base { virtual void dummy() {} }; // 虚函数确保多态性 class Derived : public Base {}; Base* basePtr = new Derived(); Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 安全的向下转型
- 使用关键字
-
重新解释类型转换(Reinterpret Cast):
- 使用关键字
reinterpret_cast<type>(expression)
进行转换。 - 用于指针之间的低级别重新解释,可以将任何指针转换为任何其他指针类型,甚至可以将指针转换为足够大的整数类型。
- 这种转换方式非常危险,因为它不进行任何类型的检查。
示例:
int* intPtr = new int(65); char* charPtr = reinterpret_cast<char*>(intPtr); // 指针重新解释
- 使用关键字
-
常量类型转换(Const Cast):
- 使用关键字
const_cast<type>(expression)
进行转换。 - 用于移除或添加
const
属性,允许修改原本声明为const
的对象。 - 这种转换通常用于临时修改对象,然后恢复其原始的
const
状态。
示例:
const int* constIntPtr = new int(10); int* modifiableIntPtr = const_cast<int*>(constIntPtr); // 移除 const 属性
- 使用关键字
每种类型转换方式都有其特定的用途和潜在的风险。开发者在使用时应该非常小心,确保转换的安全性和正确性。
push_back 和 emplace_back区别
push_back是先创建一个临时对象再调用移动构造函数将其加入容器,而emplace_back是直接加入容器,通俗来讲就是用push_back的时候需要先创建一个对象才能加入,但emplace_back不需要。一般来说,emplace_back更高效。
class A{
private:
int a;
string s;
public:
A(int a=0,string s=""){
this->a=a;
this->s=s;
}
};
int main(){
vector<A> test;
A.push_back(A(0,""));
A.emplace_back(1,"1");
return 0;
}
容量(capacity)和大小(size)区别
容量指总共可以包含的元素个数,大小则为实际所含的元素个数
vector<int>a(50);
a[0]=1;
cout<<a.capacity();//50
cout<<a.size();//1
strcpy和memcpy的区别
-
strcpy用于将一个以null结尾的字符串从源地址复制到目标地址。它会复制整个字符串,包括null终止符,直到遇到null为止。如果源字符串长度超过目标地址所分配的内存空间,则会导致内存越界和缓冲区溢出问题。
-
memcpy用于将一段内存块从源地址复制到目标地址,可以复制任意长度的内存块,而不仅限于字符串。memcpy不会关心内存块中是否有null终止符,而只是按照给定的长度复制内存块。因此,使用memcpy时需要确保目标地址有足够的内存空间,否则也会导致缓冲区溢出问题。
编译时多态和运行时多态区别
编译时多态:在程序编译过程中出现,发生在模板和函数重载中(泛型编程)。
运行时多态:在程序运行过程中出现,发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数。
#include<文件名> 和 #include"文件名" 区别
查找文件的位置
#include<>是用于包含系统头文件的指令,通常会在编译器的标准库路径中搜索头文件。
#include“”是在当前源文件所在的目录中搜索该头文件,即本地路径,其中“”内的路径是相对于当前源文件所在的路径。
因此,如果使用#include<>指令来包含本地路径的头文件,编译器可能会找不到该头文件。反之亦然
vector、list、array区别
- vector、list内存调用堆,array调用栈
- list是链表存储,不一定连续;vector、array连续空间
- array大小固定,vector、list可变