C++11
UTF-8、UTF-16、UTF32的区别?
- UFT-8:变长编码(1~4字节),兼容 ASCII,是互联网(HTML、JSON等)和类 Unix 系统的默认编码。
- UTF-16:固定2字节,适合基本多语言平面字符,不兼容 ASCII,英文文本空间效率低于 UTF-8。
- UTF-32:固定4字节,覆盖全字符集,空间浪费严重。
什么是wchar_t、char16_t、char32_t?
- wchar_t(宽字符):早期用于支持非ASCII字符(如中文、日文),但编码依赖平台。Windows下为2字节,Linux为4字节,跨平台不一致,无法直接兼容现代 Unicode 标准。
- char16_t(UTF-16字符):C++11引入,表示 UTF-16 编码的 Unicode 字符(适合基本多语言平面字符),固定 2 字节,跨平台一致。
- char32_t(UTF-32字符):C++11引入,表示 UTF-32 编码的 Unicode 字符(覆盖全字符集),固定 4 字节,跨平台一致。
说说C++的左值和右值?
- 左值:可以出现在赋值表达式的左右侧,有名称(可以通过变量名访问)、可以取地址(使用
&
操作符)、生命周期通常超出当前表达式、可以多次使用(除非被移动)。 - 右值:只能出现在赋值表达式的右侧,匿名(没有持久的内存地址)、不能取地址(如临时对象、字面量)、生命周期通常仅限于当前表达式、可以安全地被“移动”(C++11后)。
- 纯右值(prvalue):纯粹的临时值(如字面量、临时对象)
- 将亡值(xvalue):可以被“移动”的右值(如
std::move
的结果)
- 左值引用(T&):只能绑定到左值,左值引用属于左值,可以取址
- 右值引用(T&&):只能绑定到右值,右值引用属于左值,可以取址。它的本质是一个指针,当右值引用绑定到右值时,编译器会为临时对象分配内存(通常在栈上),并将右值引用指向该内存。
- 左值常引用(const T&):因为const的修饰使该类型不会修改底层对象,因此它既可以绑定左值也可以绑定右值
- 右值常引用(const T&&):只能绑定右值,不能绑定左值,需要与左值常引用区分开来。
什么是移动语义?
std::move
是 C++11 引入的一个关键工具,用于 显式标记对象为可移动(右值),从而启用移动语义(Move Semantics),避免不必要的深拷贝,提升性能。- 它可以将任何类型的参数(左值或右值)强制转换为右值引用(
T&&
),表示该对象可以被“移动”(资源可以被“窃取”) - 它仅是类型转换工具,真正的“移动”逻辑由移动构造函数/移动赋值运算符实现。
- 被移动后的对象如果是个左值,那么在原来的作用域依然存在,但是其中的值会由移动操作函数所定义,一般为nullptr或是空。所以,如果要将左值通过移动语义传输给一个函数,那么就要保证其在后面的作用域上不再使用该对象。
说说移动构造函数?
- 移动构造是C++11标准中提供的一种新的构造方法,用来给予程序员新的构造选择,用以替换拷贝构造。
- 拷贝构造函数会将传入的参数对象进行一次深拷贝,这就会有一次拷贝对象的开销,拷贝的内存越大越耗费时间,并且进行了深拷贝,就需要给新对象分配地址空间。
- 移动构造函数可以直接对传入的对象进行浅拷贝,以此直接接管源对象空间,既不会产生额外的拷贝开销,也不会给新对象分配内存空间。提高程序的执行效率,节省内存消耗。
- 移动构造函数的参数必须是自身类型的右值引用,也就是说能调用移动构造函数的参数必然是个右值(纯右值和将亡值)。
auto、decltype的用法?
- auto:根据初始化表达式推导变量类型,计算表达式的最终类型,默认忽略顶层
const
和引用,主要用于简化变量声明。如果要添加const
和引用,只能在auto关键字上手动添加,如const auto&
int x = 10; const int& rx = x; // auto: 忽略引用和顶层 const auto a1 = x; // int auto a2 = rx; // int(忽略了引用,此时类型为const int,然后又因为忽略了顶层const,因此又转为int)
- decltype: 根据给定表达式推导其声明类型,计算表达式的最终类型,保留所有修饰符(包括引用和
const
),获取表达式的精确类型。- 如果
e
是一个变量名或类的成员变量名(如e
或x.m
),则返回该实体的声明类型(不追加&
)。int x = 10; decltype(x) a = x; // int(x 是标识符)
- 如果
e
是一个表达式(如*p
或(x)
),它会先计算表达式的值,如果原本的值有引用则去除,获取其基本类型,然后进行判断。若e
是左值 → 返回T&
,若e
是纯右值 → 返回T
,若e
是将亡值 → 返回T&&
。int y = 20; decltype(*&y) b = y; // int&(*&y 是左值) decltype(y + 1) c = 0; // int(y + 1 是纯右值) decltype(std::move(y)) d = 0; // int&&(std::move(y))是一个将亡值
decltype
对表达式只在编译时推导类型而计算,并不会真正影响到原来的变量。decltype
对于表达式的计算是故意为之的,因为这样一来,如果其中的表达式是一个函数,那么decltype就可以精准推导出其函数的返回类型了。
- 如果
说下auto定义基本类型时的规则?
-
整型:
-
默认情况下,整数字面量(如
42
)会被推导为int
-
如果数值超出
int
范围,则可能推导为long
或long long
-
可以使用后缀显式指定类型:
-
u
或U
→unsigned int
-
l
或L
→long
-
ll
或LL
→long long
-
ul
或UL
→unsigned long
auto a = 42; // int auto b = 42U; // unsigned int auto c = 42L; // long auto d = 42LL; // long long auto e = 42UL; // unsigned long
-
-
-
浮点型:
- 默认情况下,浮点数字面量(如
3.14
)会被推导为double
- 可以使用后缀显式指定类型:
-
f
或F
→float
-
l
或L
→long double
auto x = 3.14; // double auto y = 3.14f; // float auto z = 3.14L; // long double
-
- 默认情况下,浮点数字面量(如
-
字符型:
-
单引号
' '
默认推导为char
-
可以使用前缀显式指定类型:
-
L
→wchar_t
(宽字符,Unicode编码,大小为2个字节) -
u
→char16_t
(UTF-16) -
U
→char32_t
(UTF-32)auto ch = 'A'; // char auto wch = L'中'; // wchar_t auto u16ch = u'字'; // char16_t auto u32ch = U'符'; // char32_t
-
-
-
布尔型:
true
和false
会被推导为bool
-
字符串:双引号
" "
默认推导为const char*
(C 风格字符串)。如果使用std::string
初始化,则推导为std::string
。 -
也可以直接对初始值进行强制类型转换来限制auto的类型推导。
什么是万能引用和完美转发?
- 万能引用:一种特殊的引用类型,能同时绑定到左值和右值,无需重载多个版本,形式为
T&&
,但仅在模板推导时成立,也就是说T不能是具体类型。template <typename T> void foo(T&& arg) {} // arg 是万能引用 int x = 10; foo(x); // T 推导为 int&,arg 类型为 int&(绑定左值) foo(20); // T 推导为 int,arg 类型为 int&&(绑定右值)
- 完美转发:其定义为
std::forward<T>
,它在函数模板中,可以保持参数的原始值类别(左值/右值),避免转发时丢失引用属性。如果不使用forward
,arg
在函数内部始终是左值(即使传入的是右值),导致无法调用右值版本。#include <utility> template <typename T> void wrapper(T&& arg) { target(std::forward<T>(arg)); // 完美转发 } void target(int& x) { std::cout << "左值引用\n"; } void target(int&& x) { std::cout << "右值引用\n"; } int main() { int x = 10; wrapper(x); // 调用 target(int&) wrapper(20); // 调用 target(int&&) }
C++中NULL和nullptr的区别?
- NULL是一个宏定义,C中NULL为(void*)0,C++中NULL为整数0。
- 将NULL定义为0带来的一个问题是无法与整数的0区分,因为C++中允许有函数重载,若是有个a、b两个重载函数,参数分别为整数和指针,那么在传入NULL参数时,会把NULL当做整数0来看,导致错误调用了参数为整数的函数。
- nullptr可以解决这一问题,nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针类型,但不会被转换为任何整型,所以不会造成参数传递错误。
说说final和override关键字?
- Override指定了子类的这个虚函数是对父类虚函数的重写,如果函数名不小心打错了的话,编译器会进行报错。
- 当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后如果被继承或重写,编译器会报错。
C++中的智能指针?
-
智能指针会管理程序员申请的内存,在使用结束后会自动释放,防止堆内存泄漏。
-
auto_ptr:最原始的智能指针。auto_ptr采用的是独享所有权语义,一个非空的auto_ptr总是拥有它所指向的资源,转移一个auto_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空。由于支持拷贝语义,拷贝后源对象变得无效,如果程序员忽视了这点,这可能引发很严重的问题。在C++11中该指针已被弃用。
-
unique_ptr:与auto_ptr类似,采用独享所有权语义。unique_ptr提供移动语义,这在很大程度上避免了auto_ptr的错误,因为很明显必须使用std::move()进行转移,提醒程序员在这个地方发生了移动。
-
shared_ptr:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。引用计数器的变化依据如下所示。
- 每次创建类的新对象时,初始化指针并将引用计数置为1
- 当对象作为另一对象的副本而创建时,拷贝构造函数会拷贝指针并增加与之相应的引用计数
- 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数
- 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)
-
weak_ptr: 由于shared_ptr引用计数存在的问题,即互相引用形成环(环形引用),使得两个指针指向的内存都无法释放,如下所示。
class BB; class AA { public: AA() { cout << "AA::AA() called" << endl; } ~AA() { cout << "AA::~AA() called" << endl; } shared_ptr<BB> m_bb_ptr; //! }; class BB { public: BB() { cout << "BB::BB() called" << endl; } ~BB() { cout << "BB::~BB() called" << endl; } shared_ptr<AA> m_aa_ptr; //! }; int main() { shared_ptr<AA> ptr_a(new AA); shared_ptr<BB> ptr_b(new BB); cout << "ptr_a use_count: " << ptr_a.use_count() << endl; cout << "ptr_b use_count: " << ptr_b.use_count() << endl; // 下面两句导致了AA与BB的循环引用,结果就是AA和BB对象都不会析构 ptr_a->m_bb_ptr = ptr_b; ptr_b->m_aa_ptr = ptr_a; cout << "ptr_a use_count: " << ptr_a.use_count() << endl; cout << "ptr_b use_count: " << ptr_b.use_count() << endl; return 0; }
- 为了解决这个问题,C++引入了weak_ptr(弱引用),它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。
- 如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。
- weak_ptr不保证它指向的内存一定是有效的,在使用之前应先使用weak_ptr的成员函数lock来检查weak_ptr是否为空指针,避免访问非法内存。如果非空,lock会返回一个新的shared_ptr,使用者可以以此来访问对象。如果为空,则返回nullptr。正因如此,weak_ptr并不能直接访问对象,他只能通过转化为shared_ptr来使用对象。
如何使用shared_ptr和weak_ptr?
#include <iostream>
#include <memory>
class Person {
public:
std::string name;
Person(const std::string& name) : name(name) {
std::cout << "Person " << name << " constructed.\n";
}
~Person() {
std::cout << "Person " << name << " destroyed.\n";
}
void sayHello() {
std::cout << "Hello, I'm " << name << "!\n";
}
};
int main() {
std::weak_ptr<Person> weakPerson; // 声明一个 weak_ptr
{
std::shared_ptr<Person> sharedPerson = std::make_shared<Person>("Alice");
weakPerson = sharedPerson; // weak_ptr 观察 shared_ptr,但不增加引用计数
std::cout << "Use count (inside block): " << sharedPerson.use_count() << "\n";
if (auto sp = weakPerson.lock()) { // lock 尝试获取 shared_ptr
sp->sayHello();
} else {
std::cout << "Person has expired.\n";
}
}
// 此处 shared_ptr 已析构,资源已释放
std::cout << "Use count (after block): " << weakPerson.use_count() << "\n";
if (auto sp = weakPerson.lock()) {
sp->sayHello();
} else {
std::cout << "Person has expired.\n";
}
return 0;
}
如何在成员函数中返回自身的shared_ptr?
- 不能直接在普通成员函数中使用
std::make_shared
返回智能指针,如下所示。std::make_shared
本身接受一组构造参数,然后间接调用了类相对应的构造函数。如果在成员函数中用this创建的智能指针,实际上是触发了类的拷贝构造函数,它所返回的是一个新对象的智能指针,而原来的调用者对象本身仍然还是由系统管理生命周期,因此下面代码会返回两次析构函数调用的结果。#include <iostream> #include <memory> class Person { public: std::string name; Person(const std::string& name) : name(name) { std::cout << "构造 Person: " << name << "\n"; } ~Person() { std::cout << "析构 Person: " << name << "\n"; } std::shared_ptr<Person> getPtr() { return std::make_shared<Person>(*this); // 看起来像是“返回自己” } }; int main() { Person p("Alice"); auto ptr = p.getPtr(); // 你以为返回的是 p 自己的指针 }
- 可以使用工厂函数创建,注意工厂函数必须是静态成员函数。
#include <memory> #include <iostream> class Person { public: std::string name; Person(const std::string& n) : name(n) {} void greet() { std::cout << "Hi, I'm " << name << "!\n"; } // 工厂方法,返回 shared_ptr static std::shared_ptr<Person> create(const std::string& name) { return std::make_shared<Person>(name); } }; int main() { std::shared_ptr<Person> p = Person::create("Alice"); p->greet(); return 0; }
- 使用
shared_from_this()
即可以在普通成员函数中返回自身的智能指针,但同时类也需要声明继承std::enable_shared_from_this
类才可以使用该函数。#include <memory> #include <iostream> class Person : public std::enable_shared_from_this<Person> { public: std::string name; Person(const std::string& n) : name(n) {} std::shared_ptr<Person> getPtr() { return shared_from_this(); // 从 this 获取 shared_ptr } void greet() { std::cout << "Hi, I'm " << name << "!\n"; } }; int main() { auto p = std::make_shared<Person>("Bob"); auto p2 = p->getPtr(); // p2 和 p 共享同一个对象 p2->greet(); std::cout << "Use count: " << p.use_count() << "\n"; // 输出 2 return 0; }
说说STL容器中的智能指针?
具备独占所有权语义的智能指针不能在STL的容器中使用,如auto_ptr和unique_ptr,因为STL容器中的元素经常要支持拷贝、赋值操作,在这过程中auto_ptr会传递所有权,容易导致错误,而unique_ptr又不支持普通的拷贝和赋值操作,也不能用在STL标准容器中。
说说lambda函数?
利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象。每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,该实例是一个右值。lambda的优点有以下几点:
- 距离:很多人认为,让定义位于使用的地方附近很有用。这样,就无需翻阅很多页的源代码,以了解函数。另外,如果需要修改代码,设计的内容就在附近,就很好修改。
- 简洁:函数符代码要比lambda代码更加繁琐,函数和lambda的简洁程度相当。
- 功能:lambda可以访问作用域内的任何动态变量,可以采用取值、引用的形式进行捕获。
什么是声明时初始化?
C++11新增了类成员初始化新机制——声明时初始化,可以直接在类中声明数据成员时就进行初始化操作,而不用借助构造函数或者初始化列表。
C++11添加哪几种构造函数关键字?
- default关键字可以显式要求编译器生成默认构造函数,防止在调用时相关构造函数没有定义而报错。
- delete关键字可以删除构造函数、赋值运算符函数等,在使用时编译器会报错
什么是列表初始化?
列表初始化是C++ 11新引进的初始化方式,它采用一对花括号(即{})进行初始化操作。能用直接初始化和拷贝初始化的地方都能用列表初始化,而且列表初始化能对容器进行方便的初始化,因此在新的C++标准中,推荐使用列表初始化的方式进行初始化。
// 列表初始化,C++11新特性
int a{12};
string s{"123"};
vector<int> vec{1, 2, 3};
初始化列表和列表初始化的区别?
- 初始化列表是在创建类对象时,对类对象内部的数据成员进行的一种初始化方式,具体用在类的构造函数中。
class date { public: date(int hour = 0): _hour(hour), _t(hour) {} private: int _hour; Time _t; };
- 列表初始化是C++11引入的一种新的对象初始化方式,它采用一对花括号(即{})进行初始化操作。主要用在类对象定义时,为它指定初始值。
// 列表初始化,C++11新特性 int a{12}; string s{"123"}; vector<int> vec{1, 2, 3};
说说constexpr?
constexpr
是 C++11 引入的关键字,用于指示编译时常量或可在编译时求值的表达式/函数。它的核心目的是让编译器在编译阶段完成计算,从而提高运行时效率或支持某些只能在编译时使用的场景- 编译时常量:替代传统的
const
或宏定义,明确要求编译器在编译时计算值(宏定义在预编译时期替换值,而const是一个运行期常量,是否在编译期优化取决于编译器行为,没有强制要求)constexpr int size = 10; // 编译时常量 int arr[size]; // 合法,size是编译期确定的
- 编译时函数:函数可以在编译时执行(若参数是编译期常量)
constexpr int square(int x) { return x * x; } constexpr int val = square(5); // 编译时计算,结果25
说说noexpect?
-
noexcept
是 C++11 引入的关键字,用于显式声明函数不会抛出异常。它既是编译时说明符,也是运行时运算符,对代码的安全性、性能优化和移动语义有重要影响。 -
编译时说明符:作为函数修饰符,声明函数不抛出异常
void foo() noexcept; // 声明 foo() 不抛出异常 void bar() noexcept(true); // 等价于 noexcept void baz() noexcept(false); // 可能抛出异常
-
作为运行时运算符:检查表达式是否可能抛出异常
bool mayThrow = noexcept(someFunction()); // 返回 true/false
-
编译器会为
noexcept
函数生成更高效的代码(无需准备异常处理栈帧),标准库(如std::vector
)在扩容时,如果元素的移动构造函数是noexcept
,会优先使用移动而非拷贝。 -
如果函数可能抛出异常,不要强行加
noexcept
,否则一旦抛出异常程序立即终止。虚函数可以声明为noexcept,派生类的重写版本可以更严格(即noexcept
可以覆盖非noexcept
),但不能反过来。
说一下C++11的函数封装?
std::function
是 C++11 引入的一个通用函数包装器,属于<functional>
头文件。它可以存储、复制和调用任何可调用对象(如普通函数、Lambda 表达式、函数对象、成员函数等),通过类型擦除技术隐藏具体类型,统一处理不同可调用对象。std::function
在存储类成员时有点特殊,它的第一个参数必须设置为该成员函数所属类的引用类型,并且在实际调用时也需要传入具体的类对象,同时该特性也导致它可以存储类成员变量。struct Linear{ Linear(float k,float b):k_(k),b_(b){} float f(float x){return k_*x+b_;} float k_, b_; }; int main(){ std::function<float(Linear&,float)> mf = &Linear::f Linear 1(1.2,2.3); float res = mf(l,5); std::cout<<res<<std::endl; std::function<float(Linear&)> k = &Linear::k_; std::cout<<k(l)<<std::endl; }
说一下C++11的bind?
std::bind
是 C++11 引入的一个函数适配器(位于<functional>
头文件),用于部分绑定函数参数或调整参数顺序,生成一个新的可调用对象。它常与std::function
配合使用,实现更灵活的回调机制。#include <functional> #include <iostream> int add(int a, int b) { return a + b; } int main() { // 绑定第一个参数为 10,调用时只需传递第二个参数 auto bound_add = std::bind(add, 10, std::placeholders::_1); std::cout << bound_add(5); // 等价于 add(10, 5),输出 15 }
- 在将变量绑定为函数参数时,默认按值绑定,若需绑定引用,使用
std::ref
或std::cref
void update(int& x) { x++; } int val = 0; auto bound_update = std::bind(update, std::ref(val)); bound_update(); // val 变为 1
什么是 emplace_back?
emplace_back
是标准库容器(如std::vector
、std::deque
、std::list
等)提供的一个成员函数,用于在容器末尾直接构造一个元素,而不是先创建临时对象再拷贝或移动到容器中。- 对于非平凡类型(如需要动态分配资源的类),
emplace_back
通常比push_back
更高效,尤其是当对象的拷贝或移动成本较高时。
什么是原子类型和原子操作?
- 原子类型是由标准库提供的一组类型,用于支持原子操作。这些类型确保对它们的操作是不可中断的原子操作。
- 原子操作总共有三种:读(load)、写(store)、Read Modify Write(RMW,先读取后修改)
- STL提供了
atomic_flag
和atomic
模板,atomic_flag
是无锁的原子类型,STL保证了其操作是无锁的,而其他类型都有可能在底层使用了锁操作。 - 要查看
atomic
类型在底层是否使用了锁,可以使用其is_lock_free
函数来查看。一般来说,基本类型和指针都是无锁的,而自定义的类型则与类型大小和内存对齐有关。因此,不推荐使用atomic
来包装复杂类型。
说说atomic_flag?
atomic_flag
是一个布尔值类型的原子变量,但它不提供普通的load()
、store()
接口- 它支持两个操作:
- test_and_set:返回原值并设置为true
- clear:设置为false
- 它在初始化时需要使用
ATOMIC_FLAG_INIT
宏来定义为false
状态。 - 它的操作是原子的,且非常适合用于构建低级别的同步原语(比如自旋锁),如下是使用
atomic_flag
实现的自旋锁。#include <atomic> class SpinLock { public: void lock(){ while (flag.test_and_set(std::memory_order_acquire)) {} } void unlock(){ flag.clear(std::memory_order_release); } private: std::atomic_flag flag{ATOMIC_FLAG_INIT}; };
什么是CAS
- CAS(Compare-And-Swap,比较交换)是多线程并发编程中非常重要的原子操作,通常用来实现无锁算法。
- 在C++中,CAS可以使用
atmoic
类型的compare_exchage
成员函数来调用,它接收两个参数,一个期望值和一个新值。它的操作逻辑是:- 如果当前值等于期望值,则将当前值修改为新值,然后返回true。
- 如果当前值不等于期望值,则将期望值修改为当前值的最新值,然后返回false。
compare_exchange
函数有两个版本,分别为strong版本和weak版本。weak版本可能会报出假false,但性能更高,而strong不会报出假fasle- 这个操作的用法一般是在循环的判断中,如下所示。可以简单的理解为,如果当前值被其他线程修改了,则读取最新值,执行相应的操作后再进行判断当前值是否被其他线程修改,如果没用被修改,则将当前值赋予新值。
#include <atomic> int fetch_multiply(std::atomic<int> &value,int multiplier) { int oldvalue = value.load(); int desired; do { desired = oldvalue * multiplier; } while(!value.compare_exchange_strong(oldvalue, desired)); return oldvalue; }
什么是线程局部变量?
thread_local
是 C++11 引入的关键字,用于定义线程局部存储变量,常用于高并发场景中。- 编译器在编译时会为每个
thread_local
变量分配一个偏移量,而每个线程的TCB(线程控制块)中会保存自身的TLS(线程局部存储区)的基地址。 - 运行时系统会根据当前线程的TLS基地址,加上线程局部变量所对应的偏移量来访问具体变量,每个线程对同一个
thread_local
变量访问的是属于自己的那一份,这样就保证了线程之间数据互不干扰,也不需要加锁。 - 这个机制主要依赖于编译器、线程库(如 pthread)和操作系统三者的协同。
什么是lock_guard和unique_lock?
- 在 C++ 中,
unique_lock
和lock_guard
都是用来管理 互斥锁(mutex) 的 RAII(资源获取即初始化)机制的工具,用于防止多线程环境下的数据竞争和死锁,但它们各有特点和适用场景。 - lock_guard:简单轻量,构造时自动加锁,析构时自动解锁。不能显式 unlock,也不能转移所有权(不可移动)。一般用于临界区非常简单的情况。
- unique_lock:支持延迟加锁(defer_lock)、提前解锁(unlock)、重新加锁(lock)、条件变量(std::condition_variable),可以转移所有权(可移动,不可复制),比lock_guard性能要低。
其他问题
怎么使用vim?
- vim是linux下的文本编辑器,具有三种模式:命令模式、底行模式、插入模式
- 在命令模式下,可以进行光标移动、文本复制(yy)、文本粘贴(p)、文本删除(dd)等操作。
- 在底行模式下,可以对文本文件进行设置,如保存(:w)、退出(:q)等操作。
- 在插入模式下,可以对文本进行编辑操作。
- 一般情况下,在进入底行模式和插入模式之前必须经过命令模式,不能直接切换
- 底行模式->插入模式, 按esc到命令模式,按i/a/o到插入模式
- 插入模式->底行模式, 按esc到命令模式,按:到底行模式
- 无论当前处于什么模式,按esc一定可以回到命令模式
怎么使用gcc/g++?
- gcc/g++是linux下常用的C/C++编译工具,gcc是C语言编译器,g++是C++编译器。通过该工具,可以完成对C/C++代码文件的预处理、编译、汇编、链接等操作。
- 安装了gcc/g++之后,可直接在命令行下调用此工具。若不加入任何参数,默认对代码文件完成所有编译工作,生成名为“a.out”的可执行文件。
- 下面介绍gcc/g++命令的常用参数:-E只执行预处理工作并生成.i文件、-S只执行编译工作并生成.s文件、-c(小写)只执行汇编工作并生成.o文件、-o指定经过gcc/g++处理后的文件名。
- 对于几个常用参数的文件后缀的记忆方法:“gcc-E,-S,-c”刚好对应键盘左上角的esc键,而对应生成的文件后缀分别为“i、s、o”,组合起来刚好是一个镜像文件的后缀。
- 链接操作没有单独的参数,在执行gcc/g++时,只要不加“E、S、c”等参数,默认会进行链接。
怎么使用gdb?
- gdb是linux下的C/C++程序调试器。要使用gdb调试,其程序必须是debug模式。gcc/g++默认生成的程序是release模式,必须在源代码生成程序时加上“-g”选项,以debug模式输出程序,才能使用gdb。
- gdb的常用操作有:list/l 行号(显示指定行号开始的10行代码)、list/l 函数名(显示指定函数的代码)、r/run(运行程序)、n /next(逐语句执行,不进入函数)、s/step(逐过程执行,会进入函数)、break/b行号(在某一行设置断点)、print/p表达式(打印表达式的值)、continue/c(继续执行程序,直到下个断点)、display 变量名(跟踪查看变量)、set变量名(设置变量值)、breaktrace/bt(查看调用堆栈)、quit(退出)
怎么使用make/Makefile?
- make是一个命令,Makefile是一个文件。两者搭配使用,可以完成项目的自动化构建并生成可执行程序,无需手动调用gcc/g++。
- Makefile文件属于一种特殊的代码文件,其代码描述了各种中间文件(比如.o文件)之间的先后依赖关系,语法较为复杂,这里不展开细讲。
- make命令会自动调用gcc/g++,让他们依照Makefile文件中的依赖关系逐一生成文件,并最后输出可执行文件。
- make clean命令用于清除已生成的可执行文件。
怎么制作并使用静态库?
- 在linux下制作静态库需要借助gcc/g++,以及归档工具ar(相当于tar、zip等解压缩工具)。
- 首先需要将所有的代码源文件编译成.o文件,再使用ar命令将所有.o文件打包在一起,就生成了一个静态库。
- 在linux下,静态库的后缀为.a,且库名称前必须加上“lib”,否则链接静态库会出错。
- 要想使用静态库,只需要在对使用静态库的源文件进行编译链接时,加上几个参数即可,如下图所示
怎么制作并使用动态库?
- 使用gcc/g++生成动态库的.o文件时,需要加上-fPIC选项,如“g++ -fPIC -c test.c -o test.o”。
- 在生成库文件时,无需使用ar,只需要使用gcc/g++加上-shared选项,如“gcc -shared test.o -o test.so”,即可生成动态库。
- 在linux下,动态库的后缀为.so,且库名称前必须加上“lib”,否则链接动态库会出错。
- 动态库使用方式与静态库相同,在对使用动态库的源文件进行编译链接时,需要加上同样的参数,以指明库文件和头文件的搜索路径以及库名称。
- 与静态库不同的是在运行程序时,还必须告诉操作系统动态库路径,指明方法有三个:
- 修改环境变量LD_LIBRARY_PATH,添加库文件路径,但下次重启系统会失效。
- 将库文件和头文件放到系统的默认目录下。
- 修改系统配置文件的动态库搜索路径。
如何使用Valgrind?
- Valgrind用于在Linux下检测内存泄露。
- 用 Valgrind 检测内存泄漏,需在编译时添加
-g
生成调试信息,然后运行下面代码。其中--leak-check=full
表示显示泄漏的详细位置(在哪个文件的哪一行)。g++ -g your_program.cpp -o your_program # 编译时加 -g valgrind --leak-check=full ./your_program
- 泄露信息关键字段:
definitely lost
:明确泄露,未释放内存,必须修复indirectly lost
:由于其他内存块(definitely lost
)的泄漏,导致依赖它的内存块也无法被访问,通常出现在嵌套数据结构中(如链表、树的结构体未完全释放)。possibly lost
:指针指向已分配内存的中间地址(而非起始地址),导致 Valgrind 无法确定是否故意为之,需要人工检查是否真实泄漏。still reachable
:程序结束时,某些内存块未被free()
或delete
,但仍有全局变量、静态变量或某些数据结构(如主线程的堆栈)持有它们的指针。例如:全局变量char* ptr = malloc(100);
未释放,但程序退出时ptr
仍指向该内存,这可能是有意保留的,需要人工检查。suppressed
:被用户标记为忽略的泄漏(通过 suppression 文件)
如何使用Dr.Memory?
-
Dr.Memory可以在Windows或Linux中检查内存泄漏,在命令行中运行:
drmemory [选项] -- <被测程序名> [被测程序参数]
-
运行后,Dr. Memory 会生成报告,包含以下常见错误:
- UNINITIALIZED READ:读取未初始化内存
- INVALID READ/WRITE:越界访问
- LEAK:内存泄漏
- POSSIBLE LEAK:潜在内存泄漏
pstack是什么?
- pstack是Linux下用于快速查看某个进程里每个线程正在执行什么函数,适用于程序卡住、死循环、线程阻塞等问题的定位。
- 使用步骤:
- 找到目标进程的 PID,可以使用ps或top命令查找。
- 执行
pstack <pid>
可以显示某个进程所有线程的当前函数调用栈
pstack
是查看“当前状态”,它不是采样工具,可以把输出保存到文件方便分析,如pstack 12345 > stack.log
。
perf是什么?
perf
是 Linux 下非常强大的性能分析工具,尤其擅长CPU 性能采样,用来找程序的性能瓶颈。- 实时查看系统或某个程序的热点函数(类似
top
,但显示的是 CPU 占用最高的函数):perf top
- 采样某个进程运行情况,生成报告(-p指定采样进程,-g表示采集调用栈信息,sleep表示10秒后停止):
perf record -p 12345 -g -- sleep 10
,然后执行perf report
会看到耗费 CPU 时间最多的函数和调用关系 - 采样运行某个程序(从运行开始):
perf record -g ./my_program perf report
cpu占用率过高怎么定位?
- 当一个项目 CPU 占用高而请求不多时,大概率是程序内部出现死循环、锁竞争或资源泄漏等问题,可通过
pstack/perf
等工具锁定高占用线程和函数栈,定位异常代码进行优化。 - 如果是线上问题,优先考虑使用
kill -STOP
命令暂停进程,检查进程状态和资源占用。如果要恢复进程运行,可以执行kill -CONT
命令。
系统调用和库函数的区别
- 系统调用是操作系统提供的操作接口,执行系统调用需要从用户态切换到内核态,以此直接请求操作系统服务(如文件读写、进程管理)
- 库函数是函数库提供的函数,运行在用户态,它的底层是对系统调用的封装和功能扩展。
- 以文件读写为例,
read(fd, buf, size)
是 系统调用(直接向 OS 请求读取文件原始字节),fgets(buf, size, fp)
是 库函数(封装了read
,加入了缓存处理,可以按行读取文本,自动加'\0')。再比如,write
是系统调用,printf
是库函数,底层使用了write
。
所有系统调用都会阻塞吗?
并非所有系统调用都会阻塞。是否阻塞取决于资源状态和调用方式,比如 I/O 操作如果资源不可用时会阻塞,但像 getpid()
、time()
等不会阻塞;同时,许多 I/O 系统调用也可以通过设置非阻塞标志(如 O_NONBLOCK)避免阻塞。
读取文件时,文件内容存储在哪个区?
- 读取文件时,文件内容并不是自动属于C++程序内存结构中的某个固定“区”,而是取决于你如何读取文件、读入的数据存到哪里。
- 通常会先申请一块内存(比如用
new
、malloc
、定义数组等),然后把文件内容读入这块内存。
有没有什么方法可以不用read操作也可以读取文件?
- mmap(内存映射 I/O)可以把文件“映射”到一段内存地址上,像访问内存一样访问文件内容,无需显式
read()
,也不用自己开 buffer,效率高。 - mmap的底层原理是:操作系统通过虚拟内存机制,将文件的内容映射到进程的虚拟地址空间中,进程可以像访问普通内存一样访问文件内容。映射完成后,操作系统并不会立刻将整个文件读入内存,而是在程序第一次访问某个地址时触发缺页异常(page fault),再按需从磁盘将对应的文件页加载到物理内存中,并建立虚拟页到物理页的映射关系。由于省去了显式的读写操作和数据拷贝,
mmap
在处理大文件或需要高效随机访问时性能更优。
评论