【C++】06.C++11、异常处理、设计模式、错误调试
【嵌入式八股】一、语言篇(本专栏)https://www.nowcoder.com/creation/manager/columnDetail/mwQPeM
【嵌入式八股】二、计算机基础篇https://www.nowcoder.com/creation/manager/columnDetail/Mg5Lym
【嵌入式八股】三、硬件篇https://www.nowcoder.com/creation/manager/columnDetail/MRVDlM
【嵌入式八股】四、嵌入式Linux篇https://www.nowcoder.com/creation/manager/columnDetail/MQ2bb0
其他
133.在main执行之前和之后执行的代码可能是什么?
main函数执行之前,主要就是初始化系统相关资源:
- 设置栈指针
- 初始化静态
static
变量和global
全局变量,即.data
段的内容 - 将未初始化部分的全局变量赋初值:数值型
short
,int
,long
等为0
,bool
为FALSE
,指针为NULL
等等,即.bss
段的内容 - 全局对象初始化,在
main
之前调用构造函数,这是可能会执行前的一些代码 - 将main函数的参数
argc
,argv
等传递给main
函数,然后才真正运行main
函数 __attribute__((constructor))
main函数执行之后:
- 全局对象的析构函数会在main函数之后执行;
- 可以用
atexit
注册一个函数,它会在main 之后执行; __attribute__((destructor))
134.解释一下什么是trivial destructor
在C++中,当一个类的析构函数不需要做任何额外的工作时,我们称这个类的析构函数为trivial destructor。也就是说,如果这个类不持有任何资源,比如堆内存、文件句柄等等,那么它的析构函数就是trivial destructor。
当一个类的析构函数是trivial destructor时,编译器会对其进行优化,使得在该类对象被销毁时,不会调用析构函数。这种优化被称为trivial destructor优化,它可以提高程序的性能和效率。
需要注意的是,如果一个类的析构函数不是trivial destructor,那么在该类对象被销毁时,析构函数必须被调用。这是因为该类对象可能持有一些资源,需要在销毁时释放。如果析构函数没有被调用,这些资源可能会泄漏,导致程序出现严重的错误。
以下是一个使用了trivial destructor的示例:
class MyClass
{
public:
int x;
// trivial destructor
~MyClass() = default;
};
int main()
{
MyClass obj;
obj.x = 10;
// do something with obj
return 0;
}
在上面的代码中,MyClass的析构函数是trivial destructor,因为它没有持有任何资源,所以编译器会对其进行优化。在主函数中,我们创建了一个MyClass对象,并为其成员变量x赋值,然后程序结束,obj对象被销毁。由于析构函数是trivial destructor,所以在obj对象被销毁时,不会调用析构函数。
C++强制类型转换函数有哪些
C++四种强制类型转换介绍 - 知乎 (zhihu.com)
C++中强制类型转换函数有4个:
const_cast (用于去除const属性)
static_cast (用于基本类型的 强制转换 )
dynamic_cast (用于多态类型之间的类型转换)
reinterpreter_cast (用于不同类型之间的指针之间的转换,最常用的就是不同类型之间 函数指针 的转换)
92.static_cast比C语言中的转换强在哪里?
- 更加安全;
- 更直接明显,能够一眼看出是什么类型转换为什么类型,容易找出程序中的错误;可清楚地辨别代码中每个显式的强制转;可读性更好,能体现程序员的意图。
C++11新标准
138.C++ 11有哪些新特性?
- 智能指针:引入了unique_ptr和shared_ptr等智能指针,可以帮助程序员更加方便和安全地管理内存。
- 右值引用:引入了新的引用类型,可以绑定到右值(临时对象、返回值等)。
- Lambda表达式:一种简洁的方式来定义匿名函数,可以捕获外部变量,使得代码更加简洁。
- nullptr关键字:可以用来表示空指针,避免了NULL常量的一些问题。
- 自动类型推导(auto):可以让编译器根据变量初始化的表达式自动推导出变量的类型。
- 静态断言(static_assert):可以在编译时进行断言,帮助程序员发现一些潜在的错误。
- 类型别名(type alias):可以使用using关键字定义类型别名,使得类型名更加易读易写。
- range-based for循环:一种更简洁的循环语法,可以遍历一个区间中的所有元素。
- constexpr关键字:可以让函数或变量在编译时求值,提高程序性能。
- 并发编程支持:引入了线程库和原子操作等特性,方便程序员进行并发编程。
记忆:右能Lna
141.说说你了解的auto_ptr作用
auto_ptr
是一个 C++ 标准库中的智能指针,它的主要作用是自动管理一个对象的生命周期。具体来说,auto_ptr
对象拥有对一个对象的独占所有权,并且会在该对象不再需要时自动释放它。
auto_ptr
的实现方式是利用 RAII(Resource Acquisition Is Initialization)技术,即资源获取即初始化。它在构造函数中获取指向对象的指针,并在析构函数中释放该指针所指向的对象。因此,当一个 auto_ptr
对象超出其作用域或被销毁时,它所管理的对象也会被自动释放。
需要注意的是,由于 auto_ptr
是独占所有权,所以它不能与其他 auto_ptr
对象共享同一个对象。而且,由于 auto_ptr
实现的不够完善,在某些情况下可能会导致问题,例如多个 auto_ptr
对象指向同一个对象,或者将 auto_ptr
对象转移到函数之外等。
在 C++11 标准中,auto_ptr
被弃用了,并被 unique_ptr
替代。unique_ptr
拥有与 auto_ptr
相同的作用,但实现更为完善,不会存在 auto_ptr
存在的一些问题。
142.C++左值引用和右值引用
为什么C/C++等少数编程语言要区分左右值? - 知乎 (zhihu.com)
C++新标准001_“左左右右分不清”右值引用_哔哩哔哩_bilibili
C++11正是通过引入右值引用来优化性能,具体来说是通过移动语义来避免无谓拷贝的问题,通过move语义来将临时生成的左值中的资源无代价的转移到另外一个对象中去,通过完美转发来解决不能按照参数实际类型来转发的问题(同时,完美转发获得的一个好处是可以实现移动语义)。
左值和右值
==左值:表示的是可以获取地址的表达式,它能出现在赋值语句的左边,对该表达式进行赋值。==但是修饰符const的出现使得可以声明如下的标识符,它可以取得地址,但是没办法对其进行赋值
const int& a = 10;
==右值:表示无法获取地址的对象,有常量值、函数返回值、lambda表达式等。无法获取地址,但不表示其不可改变,当定义了右值的右值引用时就可以更改右值。==
左值引用和右值引用
左值引用:传统的C++中引用被称为左值引用
右值引用:C++11中增加了右值引用,右值引用关联到右值时,右值被存储到特定位置,右值引用指向该特定位置,也就是说,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置
这里主要说一下右值引用的特点:
- 特点1:通过右值引用的声明,右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去
- 特点2:右值引用独立于左值和右值。意思是右值引用类型的变量可能是左值也可能是右值
- 特点3:T&& t在发生自动类型推断的时候,它是左值还是右值取决于它的初始化。
举个例子:
#include <bits/stdc++.h>
using namespace std;
template<typename T>
void fun(T&& t)
{
cout << t << endl;
}
int getInt()
{
return 5;
}
int main() {
int a = 10;
int& b = a; //b是左值引用
int& c = 10; //错误,c是左值不能使用右值初始化
int&& d = 10; //正确,右值引用用右值初始化
int&& e = a; //错误,e是右值引用不能使用左值初始化
const int& f = a; //正确,左值常引用相当于是万能型,可以用左值或者右值初始化
const int& g = 10; //正确,左值常引用相当于是万能型,可以用左值或者右值初始化
const int&& h = 10; //正确,右值常引用
const int& aa = h; //正确
int& i = getInt(); //错误,i是左值引用不能使用临时变量(右值)初始化
int&& j = getInt(); //正确,函数返回值是右值
fun(10); //此时fun函数的参数t是右值
fun(a); //此时fun函数的参数t是左值
return 0;
}
143.什么是右值引用,跟左值又有什么区别?
主要区别在于:
- 左值引用只能绑定到一个左值(即具有名称或者在内存中有存储地址的对象),而右值引用只能绑定到一个右值(即没有名称或者内存中没有存储地址的临时对象)。
- 左值引用可以被用作修改其绑定对象的值的目标,而右值引用只能被用作读取其绑定对象的值的源。
- 左值引用可以通过取地址运算符(&)取得其绑定对象的地址,而右值引用不能。
在 C++ 中,左值引用的类型是 T&
,右值引用的类型是 T&&
,其中 T 是任意类型。左值引用可以用来实现函数的参数传递和返回值传递,==而右值引用则通常用来实现移动语义和完美转发。==
- 在move语义中,右值引用可以将一个对象的资源(如内存、文件句柄等)所有权从一个对象转移到另一个对象,从而避免了不必要的复制和销毁操作,提高了程序的效率。
- 在完美转发中,右值引用可以将参数完全转发到另一个函数中,从而避免了额外的复制和移动操作,提高了程序的灵活性和可维护性。
需要注意的是,右值引用只能绑定到一个将要被销毁的临时对象或者一个没有名字的对象,因此在使用右值引用时需要特别小心,以避免出现悬空引用或者非法访问的情况。
右值引用与move语义?
右值引用和移动语义是C++11引入的两个重要特性,用于优化对象的拷贝和资源管理。它们通常一起使用,以提高性能和减少不必要的开销。
右值引用是对右值的引用类型,通过使用双引号(&&)来声明。右值引用可以绑定到临时对象(右值)或即将被销毁的对象,而不能绑定到左值(非临时对象)。右值引用的主要特点是能够区分左值和右值,并在语言层面上提供了一种安全和高效的转移语义。
move语义是一种通过使用右值引用实现的语义,允许资源(如堆内存)在对象之间转移而不是复制。移动操作将一个对象的资源所有权从一个对象转移到另一个对象,避免了昂贵的拷贝操作,并减少了动态内存分配和释放的开销。
使用move语义时,可以使用std::move()
函数将左值转换为右值引用,以便进行资源的移动操作。移动语义通常与移动构造函数和移动赋值操作符一起使用,它们接受右值引用作为参数,并将资源从传入的对象转移到目标对象。移动操作之后,源对象处于有效但不可靠的状态。
移动语义在以下情况下特别有用:
- 在函数返回值时,可以通过移动返回值避免额外的复制开销。
- 在函数参数传递时,可以通过移动参数将资源传递给函数而不进行额外的拷贝。
- 在容器操作中,可以通过移动元素来提高容器的性能。
总结来说,右值引用和移动语义是C++11引入的重要特性,通过引入新的引用类型和语义来优化对象的拷贝和资源管理。它们使得代码更高效、性能更好,并提供了更灵活的资源转移机制。
C++move函数?
在C++中,std::move
是一个函数模板,位于<utility>
头文件中,用于将对象转移(移动)为右值引用。
std::move
的定义如下:
template <typename T>
typename std::remove_reference<T>::type&& move(T&& arg) noexcept;
std::move
接受一个参数arg
,该参数可以是任意类型的左值引用或右值引用。在函数模板的实例化过程中,通过引用折叠规则,参数arg
被转换为右值引用。
std::move
的作用是将参数转化为右值引用,这样可以告诉编译器该对象可以被移动而不是复制。移动操作通常比复制操作更高效,因为它可以避免不必要的资源拷贝。
使用std::move
的典型场景是在移动语义中,例如在移动构造函数和移动赋值运算符中。示例如下:
class Example {
public:
Example(Example&& other) noexcept
: data(std::move(other.data)) {
// 执行移动构造逻辑
}
Example& operator=(Example&& other) noexcept {
if (this != &other) {
data = std::move(other.data);
// 执行移动赋值逻辑
}
return *this;
}
private:
// 数据成员
};
在上面的代码中,通过使用std::move
,将other.data
转化为右值引用,从而执行资源的移动操作。
需要注意的是,使用std::move
只是告诉编译器对象可以被移动,但并不会自动执行移动操作。具体的移动操作需要在移动构造函数或移动赋值运算符中自行实现。
C++中的Lambda表达式?
清晰易懂,现代C++最好用特性之一:Lambda表达式用法详解_哔哩哔哩_bilibili
C++中的Lambda表达式是一种匿名函数/闭包,它可以在需要函数的地方使用,而无需显式地定义一个命名函数。Lambda表达式提供了一种更方便和灵活的方式来编写简短的函数,尤其是用于函数对象、算法和函数式编程的场景。
下面是一个基本的Lambda表达式的语法结构:
[capture list](parameter list) -> return type {
// 函数体
}
-
capture list
(捕获列表):用于指定Lambda表达式中使用的外部变量。可以通过值捕获或引用捕获方式来捕获变量。例如,[x]
表示按值捕获变量x
,[&y]
表示按引用捕获变量y
。还可以使用捕获初始化器来指定初始值,例如[x = 42]
表示按值捕获变量x
并将其初始化为42。 -
parameter list
(参数列表):Lambda函数的参数列表,类似于普通函数的参数列表。参数可以省略类型,编译器可以进行类型推导。 -
return type
(返回类型):Lambda函数的返回类型。可以省略返回类型,编译器可以根据函数体中的表达式进行推导。 -
{}
(函数体):包含Lambda函数的具体实现。
以下是一个示例,展示了Lambda表达式的用法:
#include <iostream>
int main() {
int x = 5;
int y = 10;
// Lambda表达式示例:将两个数相加并输出结果
auto sum = [x, &y]() -> int {
return x + y;
};
std::cout << "Sum: " << sum() << std::endl;
return 0;
}
在上面的示例中,Lambda表达式使用捕获列表 [x, &y]
捕获了变量 x
(按值捕获)和 y
(按引用捕获)。Lambda函数的返回类型通过 -> int
指定为 int
类型,然后在函数体中计算了 x + y
的和并返回。
注意,Lambda表达式可以在需要函数的地方使用,例如可以将其传递给STL算法函数、作为函数对象使用等。Lambda表达式提供了一种便捷的方式来编写短小的、临时的函数代码,从而增加了代码的可读性和灵活性。
139.auto、decltype和decltype(auto)的用法
auto、decltype和decltype(auto)都是C++11中引入的关键字,它们都与类型推导相关。
- auto关键字
auto关键字可以让编译器根据变量初始化的表达式自动推导出变量的类型。例如:
auto x = 1; // 推导出x的类型为int
auto p = new int(10); // 推导出p的类型为int*
auto关键字通常用于简化代码,同时可以避免类型冗余和代码维护的复杂性。
- decltype关键字
decltype关键字可以推导出表达式的类型,而不需要实际计算出表达式的值。例如:
int x = 10;
decltype(x) y = x; // 推导出y的类型为int,与x的类型相同
decltype关键字常用于泛型编程和模板元编程中,可以让程序员在编译时进行类型检查和类型转换。
- decltype(auto)关键字
decltype(auto)关键字结合了auto和decltype的特性,可以自动推导出表达式的类型,并且保留表达式的引用类型和cv限定符。例如:
int x = 10;
int& foo() { return x; }
decltype(auto) y = foo(); // 推导出y的类型为int&,与foo()的返回类型相同
decltype(auto)关键字可以避免类型丢失和类型转换,同时保留了引用类型和cv限定符,使得代码更加安全和灵活。
140.C++中NULL和nullptr区别
在C++11之前,通常使用NULL
宏来表示空指针,它被定义为整数零,例如:
int* p = NULL;
然而,NULL
宏实际上是一个整数,而不是指针类型,因此在某些情况下可能会导致意外的行为。例如,下面的代码会编译通过,但实际上会导致段错误:
int* p = NULL;
*p = 10; // 取消引用空指针,导致段错误
C++11引入了一个新的关键字nullptr
来表示空指针,它是一个明确定义的指针类型,例如:
int* p = nullptr;
使用nullptr
关键字可以避免一些指针类型的错误,例如取消引用空指针,同时使代码更加清晰和易读。
异常处理
C++的异常处理的方法
在程序执行过程中,由于程序员的疏忽或是系统资源紧张等因素都有可能导致异常,任何程序都无法保证绝对的稳定,常见的异常有:
- 数组下标越界
- 除法计算时除数为0
- 动态分配空间时空间不足
- ...
如果不及时对这些异常进行处理,程序多数情况下都会崩溃。
(1)try、throw和catch关键字
C++中的异常处理机制主要使用try、throw和catch三个关键字,其在程序中的用法如下:
#include <iostream>
using namespace std;
int main()
{
double m = 1, n = 0;
try {
cout << "before dividing." << endl;
if (n == 0)
throw - 1; //抛出int型异常
else if (m == 0)
throw - 1.0; /
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
查阅整理上千份嵌入式面经,将相关资料汇集于此,主要包括: 0.简历面试 1.语言篇【本专栏】 2.计算机基础 3.硬件篇 4.嵌入式Linux (建议PC端查看)