目录
1、列表初始化
2、隐式类型转换
1)概念理解
2)举例增进理解
3)隐式与显式区别?
a、直接初始化 vs 拷贝初始化
b、构造函数调用
c、语义上的差异
d、性能差异
4)explicit 关键字
5)多参数的隐式类型转换?
a、概念理解
b、举例增进理解
3、initializer_list参数
1)概念理解
2)initializer_list的底层
3)用法
4、decltype关键字
1)概念理解
2)用法
5、右值引用
1)语法格式
2)概念理解
3)转移构造
4)完美转发
5)移动赋值
6、万能引用
7、lambada表达式
1)格式
2)细节
8、模板可变参数
9、包装器
1、列表初始化
不是初始化列表(构造函数部分)
目的:一切都可以使用列表初始化,可以省略赋值符号=
//初始化列表形式的初始化,但是没有什么很实际的用处
int a{1};
char ch{'a'};
string s{ "hello,world" };
vector<int>v{1,2,3,4,5};
list<string>s{"math","China","English"};
2、隐式类型转换
1)概念理解
什么是隐式类型转换?
2)举例增进理解
理解了吗?我想没有。举一个例子:
class A {
public:
A(int a)
:_a(a)
{
}
private:
int _a;
};
int main()
{
A a1 = 1;
return 0;
}
这里有一个自定义类型A,他的成员变量只有一个int
按理来说,A是自定义类型,1是int类型,是不可以这样定义的。
正常的初始化A是什么样的呢?
应该是这样子的:
//显式调用了A的构造函数,然后再将构造出来的这个对象,拷贝赋值给我们的a
A a = A(1);
这是显式的调用了A的构造函数,然后再将构造出来的这个对象,再拷贝赋值给我们的对象a
也就是说,这简单的一个语句,经过2个阶段:
1、构造函数构造对象
2、拷贝赋值给a(但是事实上,不一定会使用拷贝复制,可能会优化,这个要看编译器,或者直接使用移动构造)
而第一种初始化方式:
//编译器使用A的构造函数将整数 1 转换为类型 A 的临时对象
A a1 = 1;
他没有按照正常的流程走
没有显式调用构造,而是直接给了一个int值1
那么这个期间发生了什么?
这种初始化方式就是隐式类型转换,因为整数 1 被自动转换为 A 类型对象
编译器查找到了类 A 中的一个构造函数 A(int a),该构造函数接受一个整型参数。
编译器使用这个构造函数将整数 1 转换为类型 A 的临时对象。
最后,编译器使用拷贝初始化的方式将这个临时对象拷贝构造到 a1 中,完成了整个初始化过程
简单来说,也是经过了两个过程:
1、构造函数构造临时对象
2、拷贝赋值给a1
3)隐式与显式区别?
都是先构造,再拷贝,那么区别是什么?
a、直接初始化 vs 拷贝初始化
A a1 = 1; 使用的是拷贝初始化方式。
A a = A(1); 使用的是直接初始化方式。
b、构造函数调用
拷贝初始化可能会调用拷贝构造函数,将临时对象复制给目标对象。
直接初始化则是直接调用相应的构造函数来创建对象,不一定会调用拷贝构造函数。在这种情况下,也可能调用移动构造函数或其他相关的构造函数。
c、语义上的差异
拷贝初始化通常用于通过已有对象创建一个新对象,其语义上暗示着创建一个新的对象并将其初始化为与另一个对象相同的值。
直接初始化则更加明确,通常用于显式地调用特定的构造函数来创建对象。
d、性能差异
直接初始化可能比拷贝初始化更加高效,因为它不需要额外的拷贝步骤,尤其是在C++11及以后版本中,当涉及到移动语义时。
拷贝初始化可能会导致不必要的对象复制,特别是在某些情况下,例如临时对象的创建和销毁,以及不必要的复制操作。
4)explicit
关键字
explicit 是 C++ 中的一个关键字,用于修饰单参数的构造函数,用于防止隐式类型转换。当一个构造函数被声明为 explicit 时,它将不再参与隐式类型转换,只能被用于显式地创建对象。
class A {
public:
explicit A(int a)
:_a(a)
{
}
private:
int _a;
};
int main()
{
A a = 1;//会报错
return 0;
}
5)多参数的隐式类型转换?
a、概念理解
对于多参数构造函数来说,它们本身就不会参与隐式类型转换,因为在调用时必须显式地提供所有参数。隐式类型转换指的是将一种类型转换为另一种类型的过程,而不需要显式的构造函数调用。一般发生在单参数构造函数的情况下,因为单参数构造函数可以被用于将一种类型隐式地转换为另一种类型。
b、举例增进理解
举个例子,假设有一个类 MyClass 和一个单参数构造函数,可以将 int 类型的参数转换为 MyClass 类型:
class MyClass {
public:
MyClass(int x) {
// 构造函数的实现
}
};
如果你写成 MyClass obj = 10;
编译器会自动调用 MyClass 的构造函数,将 10 这个 int 类型的参数隐式地转换为 MyClass 类型。
这就是隐式类型转换。
然而,对于多参数构造函数,比如:
class MyClass {
public:
MyClass(int x, double y) {
// 构造函数的实现
}
};
如果你写下 MyClass obj = 10;
这是不合法的,因为编译器无法将 10 这个 int 类型的参数隐式地转换为 MyClass 类型,因为缺少必要的参数。
你必须显式地提供构造函数所需的所有参数,
例如 MyClass obj(10, 3.14);
MyClass obj(10, 3.14);
因此,多参数构造函数本身并不会导致隐式类型转换,因为等号后面只能跟一个参数,但这个是多参数的。你必须提供所有参数,并显式调用构造函数才能创建对象。
3、initializer_list参数
1)概念理解
initializer_list 是 C++11 中引入的一种特殊类型,用于表示一组同类型的值的列表。一般用于初始化容器类对象(如 std::vector
、std::initializer_list
等)或者自定义类型的构造函数。
例如对于vector初始化的初始化:
如果初始化数据个数是不确定的
如果按照隐式类型转换的方式来支持列表初始化
因为列表初始化的过程是:构造函数+拷贝构造
而构造函数要保证有对应匹配参数个数
这就会导致构造函数冗余,
因此设置一个参数,即initializer_list
2)initializer_list的底层
它的内部空间有两个指针
它不开空间,而是指向一个常量数组的开始数据和最后数据的下一个位置
事实上其原理很简单
就是其中一个构造函数的参数设置为initializer_list
然后对应的for循环,push_back即可
initializer_list是一个指向常量数组类型的容器
内部有两个变量,指向常量数组的开始和最后位置
3)用法
一般使用在构造函数中
class MyClass {
public:
MyClass(std::initializer_list<int> list) {
// 构造函数的实现
}
};
int main() {
MyClass obj1{1, 2, 3}; // 使用 initializer_list 初始化对象
MyClass obj2 = {4, 5, 6}; // 也可以使用赋值语句初始化对象
return 0;
}
4、decltype关键字
1)概念理解
decltype 是 C++11 中引入的一个关键字,用于获取表达式的数据类型。
其实也是为了解决auto自动推导类型的麻烦,互补(非必要不建议使用,自从c++14允许auto可以推导返回值之后,就是一个大坑)
2)用法
- 推断变量的类型
-
int x = 5; decltype(x) y; // y 的类型为 int
- 推断函数返回值的类型
-
int func() { return 10; } decltype(func()) result; // result 的类型为 int
推断表达式的类型,包括引用类型和 const 修饰
-
const int& ref = x; decltype(ref) z = y; // z 的类型为 const int&
推断模板参数的类型
-
template<typename T, typename U> auto add(T t, U u) -> decltype(t + u) { return t + u; }
- 在这个例子中,
decltype(t + u)
用于推断add
函数的返回类型,它将返回t
和u
相加后的类型。
5、右值引用
1)语法格式
int&& a = 10;//int&&是一个右值引用
int& b = a;//int&是一个左值引用
2)概念理解
右值引用是 C++11 中引入的一个特性,用于支持移动语义和完美转发。
左值:可以取地址
右值:不可以取地址,不能被赋值
左值引用就是对左值取别名,右值引用就是对右值取别名
左值能否作为右值的引用?不可以,但是const可以
右值能否作为左值的引用?不可以,但是move以后的左值可以(move:从左值转换成右值)
右值一般有:临时变量、匿名对象
左值右值都有地址,左值可以取,右值不可以取
右值引用本身是左值
交换的swap的参数是一个左值
3)转移构造
参数为右值的构造函数叫做移动构造,延长资源的生命周期,移动构造其实是资源的转移。
转移构造函数(Move Constructor)是 C++11 中引入的一个特殊构造函数,用于实现移动语义移动语义允许在不进行深拷贝的情况下将资源(如动态分配的内存)从一个对象转移到另一个对象。通过将资源的所有权从一个临时对象(右值)转移到另一个对象,可以避免额外的内存分配和拷贝操作。
对于右值来说,既然你是一个即将被销毁的值(右值),没用了。但对于另一个对象来说,你是一个待用的值。对你没用,但是对我来说正好就是要用的。所以,在你还没有被销毁之前就直接交换。这就不用再构造、拷贝。
4)完美转发
在 C++ 中,函数传递参数时,常常需要考虑到参数的值类型、引用类型、常量修饰符等。而且,有时候我们希望在函数中传递参数给其他函数时,能够保留原始参数的类型和属性,即使它是一个临时对象(右值)或者一个具名对象(左值)。
完美转发的核心作用是将参数原封不动地传递给其他函数,无论参数是左值还是右值,都能保留其值类型
格式
template<typename T>
void forwarder(T&& arg) {
target(std::forward<T>(arg));
}
forwarder
函数模板接受一个参数 arg
,并通过 std::forward
将参数完美转发给另一个函数 target
5)移动赋值
ClassName& operator=(ClassName&& other) {
if (this != &other) {
// 释放当前对象的资源
// 转移其他对象的资源到当前对象
}
return *this;
}
移动赋值的主要作用是实现对象之间的资源转移,而不进行深拷贝操作
6、万能引用
万能引用是一个在模板函数中的一种特殊的引用类型。
T&&当作为参数时,不是右值引用,而是万能引用。怎么理解?
很简单,当传递左值时,T
被推导为左值引用类型,当传递右值时,T
被推导为非引用类型。
template<typename T>
void fun(T&& arg) {
// ...
}
7、lambada表达式
1)格式
由捕捉列表,参数列表,返回值,函数体四部分构成
[capture list] (parameters) -> return_type {
// Lambda 表达式的函数体
}
capture list:捕获列表,用于捕获外部变量,可以为空或包含零个或多个变量。捕获列表通过值捕获或引用捕获外部变量。
parameters:参数列表,类似于普通函数的参数列表,可以为空或包含零个或多个参数。
return_type:返回类型,指定 Lambda 表达式的返回类型,可以省略,编译器会根据上下文自动推导返回类型。
{}:Lambda 表达式的函数体,包含具体的操作逻辑
lambada的本质是仿函数,是一个匿名函数对象
其类型不知道,只有编译器知道
为什么会有lambda表达式?
因为仿函数的是书写太麻烦了,要单独写一个类,然后再匿名对象传参
不如直接使用表达式作为参数
2)细节
捕捉列表捕捉当前域的对象(传值传参,叫做传值捕捉)
可以修改,但是修改a,b是拷贝过来的(传值),所以不改变外面的值
int main() {
int a = 10;
int b = 20;
int c = 30;
int d = 40;
【&a,&b】引用方式捕捉
// 引用捕捉:修改 a、b 会影响外部变量
auto func2 = [&a, &b]() {
a = 1000;
b = 2000;
};
【=】捕捉所有变量
// 捕捉所有变量:传值捕捉,修改 a、b 不会影响外部变量
auto func3 = [=]() mutable {
a = 10000;
b = 20000;
};
【&】引用的方式捕捉所有变量
// 引用的方式捕捉所有变量:修改 a、b 会影响外部变量
auto func4 = [&]() {
a = 100000;
b = 200000;
};
【&a,b】混合捕捉:a为引用捕捉,b为传值捕捉
// 混合捕捉:a 为引用捕捉,b 为传值捕捉
auto func6 = [&a, b]() mutable {
a = 1000000;
b = 2000000;
};
函数 = delete:不允许这个函数被调用
8、模板可变参数
可变参数模板是C++中的一个特性,它允许在模板中接受数量不定的参数。
#include <iostream>
// 基本情况:递归终止条件
void print() {
std::cout << std::endl;
}
// 可变参数模板:递归展开参数包
template<typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first << " ";
print(args...);
}
int main() {
print(1, 2, 3, 4, 5);
print("Hello", "World", "!");
return 0;
}
9、包装器
一个栈帧内部的临时对象,如果比较小,存在寄存器;比较大,存在两个栈帧之间
这个叫做压参数/压栈帧