C++学习笔记(17)

221、委托构造和继承构造
C++11 标准新增了委托构造和继承构造两种方法,用于简化代码。 一、委托构造
在实际的开发中,为了满足不同的需求,一个类可能会重载多个构造函数。多个构造函数之间可能会
有重复的代码。例如变量初始化,如果在每个构造函数中都写一遍,这样代码会显得臃肿。
委托构造就是在一个构造函数的初始化列表中调用另一个构造函数。
注意:
 不要生成环状的构造过程。
 一旦使用委托构造,就不能在初始化列表中初始化其它的成员变量。
示例:
#include <iostream>
using namespace std;
class AA
{
private:
int m_a;
int m_b;
double m_c;
public:
// 有一个参数的构造函数,初始化 m_c
AA(double c) {
m_c = c + 3; // 初始化 m_c
cout << " AA(double c)" << endl;
}
// 有两个参数的构造函数,初始化 m_a 和 m_b
AA(int a, int b) {
m_a = a + 1; // 初始化 m_a
m_b = b + 2; // 初始化 m_b
cout << " AA(int a, int b)" << endl;
}
// 构造函数委托 AA(int a, int b)初始化 m_a 和 m_b
AA(int a, int b, const string& str) : AA(a, b) {
cout << "m_a=" << m_a << ",m_b=" << m_b << ",str=" << str << endl;
}
// 构造函数委托 AA(double c)初始化 m_c
AA(double c, const string& str) : AA(c) {
cout << "m_c=" << m_c << ",str=" << str << endl;
}
};
int main()
{
AA a1(10, 20, "我是一只傻傻鸟。");
AA a2(3.8, "我有一只小小鸟。");
}
二、继承构造
在 C++11 之前,派生类如果要使用基类的构造函数,可以在派生类构造函数的初始化列表中指定。
在《126、如何构造基类》中有详细介绍。
C++11 推出了继承构造(Inheriting Constructor),在派生类中使用 using 来声明继承基类的构
造函数。
示例:
#include <iostream>
using namespace std;
class AA // 基类。
{
public:
int m_a;
int m_b;
// 有一个参数的构造函数,初始化 m_a
AA(int a) : m_a(a) { cout << " AA(int a)" << endl; }
// 有两个参数的构造函数,初始化 m_a 和 m_b
AA(int a, int b) : m_a(a), m_b(b) { cout << " AA(int a, int b)" << endl; }
};
class BB :public AA // 派生类。
{
public:
double m_c;
using AA::AA; // 使用基类的构造函数。
// 有三个参数的构造函数,调用 A(a,b)初始化 m_a 和 m_b,同时初始化 m_c
BB(int a, int b, double c) : AA(a, b), m_c(c) {
cout << " BB(int a, int b, double c)" << endl;
}
void show() { cout << "m_a=" << m_a << ",m_b=" << m_b << ",m_c=" << m_c <<
endl; }
};
int main()
{
// 将使用基类有一个参数的构造函数,初始化 m_a
BB b1(10);
b1.show();
// 将使用基类有两个参数的构造函数,初始化 m_a 和 m_b
BB b2(10,20);
b2.show();
// 将使用派生类自己有三个参数的构造函数,调用 A(a,b)初始化 m_a 和 m_b,同时初始化 m_c
BB b3(10,20,10.58);
b3.show();
}
222、lambda 函数
lambda 函数是 C++11 标准新增的语法糖,也称为 lambda 表达式或匿名函数。
lambda 函数的特点是:距离近、简洁、高效和功能强大。
示例:[](const int& no) -> void { cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n"; };
语法:
示例:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 表白函数。
void zsshow(const int & no) {
cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
}
// 表白仿函数。
class czs
{
public:
void operator()(const int & no) {
cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
}
};
int main()
{
vector<int> vv = { 5,8,3 }; // 存放超女编号的容器。
// 第三个参数是普通函数。
for_each(vv.begin(), vv.end(), zsshow);
// 第三个参数是仿函数。
for_each(vv.begin(), vv.end(), czs());
// 第三个参数是 lambda 表达式。
for_each(vv.begin(), vv.end(), [](const int& no) {
cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
}
);
} 一、参数列表
参数列表是可选的,类似普通函数的参数列表,如果没有参数列表,()可以省略不写。
与普通函数的不同:
 lambda 函数不能有默认参数。
 所有参数必须有参数名。
 不支持可变参数。 二、返回类型
用后置的方法书写返回类型,类似于普通函数的返回类型,如果不写返回类型,编译器会根据函数体
中的代码推断出来。
如果有返回类型,建议显式的指定,自动推断可能与预期不一致。 三、函数体
类似于普通函数的函数体。 四、捕获列表
通过捕获列表,lambda 函数可以访问父作用域中的非静态局部变量(静态局部变量可以直接访问,
不能访问全局变量)。
捕获列表书写在[]中,与函数参数的传递类似,捕获方式可以是值和引用。
以下列出了不同的捕获列表的方式。
1)值捕获
与传递参数类似,采用值捕获的前提是变量可以拷贝。
与传递参数不同,变量的值是在 lambda 函数创建时拷贝,而不是调用时拷贝。
例如:
size_t v1 = 42;
auto f = [ v1 ] { return v1; }; // 使用了值捕获,将 v1 拷贝到名为 f 的可调用对象。
v1 = 0;
auto j = f(); // j 为 42,f 保存了我们创建它是 v1 的拷贝。
由于被捕获的值是在 lambda 函数创建时拷贝,因此在随后对其修改不会影响到 lambda 内部的值。
默认情况下,如果以传值方式捕获变量,则在 lambda 函数中不能修改变量的值。
2)引用捕获
和函数引用参数一样,引用变量的值在 lambda 函数体中改变时,将影响被引用的对象。
size_t v1 = 42;
auto f = [ &v1 ] { return v1; }; // 引用捕获,将 v1 拷贝到名为 f 的可调用对象。
v1 = 0;
auto j = f(); // j 为 0。
如果采用引用方式捕获变量,就必须保证被引用的对象在 lambda 执行的时候是存在的。
3)隐式捕获
除了显式列出我们希望使用的父作域的变量之外,还可以让编译器根据函数体中的代码来推断需要捕
获哪些变量,这种方式称之为隐式捕获。
隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获
的方式捕获外部变量。
int a = 123;
auto f = [ = ] { cout << a << endl; }; //值捕获
f(); // 输出:123
auto f1 = [ & ] { cout << a++ << endl; }; //引用捕获
f1(); //输出:123(采用了后++)
cout << a << endl; //输出 124
4)混合方式捕获
lambda 函数还支持混合方式捕获,即同时使用显式捕获和隐式捕获。
混合捕获时,捕获列表中的第一个元素必须是 = 或 &,此符号指定了默认捕获的方式是值捕获或引
用捕获。
需要注意的是:显式捕获的变量必须使用和默认捕获不同的方式捕获。例如:
int i = 10;
int j = 20;
auto f1 = [ =, &i] () { return j + i; }; // 正确,默认值捕获,显式是引用捕获
auto f2 = [ =, i] () { return i + j; }; // 编译出错,默认值捕获,显式值捕获,冲突了
auto f3 = [ &, &i] () { return i +j; }; // 编译出错,默认引用捕获,显式引用捕获,冲突了
5)修改值捕获变量的值
在 lambda 函数中,如果以传值方式捕获变量,则函数体中不能修改该变量,否则会引发编译错误。
在 lambda 函数中,如果希望修改值捕获变量的值,可以加 mutable 选项,但是,在 lambda 函数
的外部,变量的值不会被修改。
int a = 123;
auto f = [a]()mutable { cout << ++a << endl; }; // 不会报错
cout << a << endl; // 输出:123
f(); // 输出:124
cout << a << endl; // 输出:123
6)异常说明
lambda 可以抛出异常,用 throw(…)指示异常的类型,用 noexcept 指示不抛出任何异常。 五、lambda 函数的本质
当我们编写了一个 lambda 函数之后,编译器将它翻译成一个类,该类中有一个重载了()的函数。
1)采用值捕获
采用值捕获时,lambda 函数生成的类用捕获变量的值初始化自己的成员变量。
例如:
int a =10;
int b = 20;
auto addfun = [=] (const int c ) -> int { return a+c; };
int c = addfun(b);
cout << c << endl;
等同于:
class Myclass
{
int m_a; // 该成员变量对应通过值捕获的变量。
public:
Myclass( int a ) : m_a(a){}; // 该形参对应捕获的变量。
// 重载了()运算符的函数,返回类型、形参和函数体都与 lambda 函数一致。
int operator()(const int c) const
{
return a + c;
}
};
默认情况下,由 lambda 函数生成的类是 const 成员函数,所以变量的值不能修改。如果加上 mut
able,相当于去掉 const。这样上面的限制就能讲通了。
2)采用引用捕获
如果 lambda 函数采用引用捕获的方式,编译器直接引用就行了。
唯一需要注意的是,lambda 函数执行时,程序必须保证引用的对象有效。
225、右值引用
一、左值、右值
在 C++中,所有的值不是左值,就是右值。左值是指表达式结束后依然存在的持久化对象,右值是
指表达式结束后就不再存在的临时对象。有名字的对象都是左值,右值没有名字。
还有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右
值。
C++11 扩展了右值的概念,将右值分为了纯右值和将亡值。
 纯右值:a)非引用返回的临时变量;b)运算表达式产生的结果;c)字面常量(C 风格字符串
除外,它是地址)。
 将亡值:与右值引用相关的表达式,例如:将要被移动的对象、T&&函数返回的值、std::move
()的返回值、转换成 T&&的类型的转换函数的返回值。
不懂纯右值和将亡值的区别其实没关系,统一看作右值即可,不影响使用。
示例:
class AA {
int m_a;
};
AA getTemp()
{
return AA();
}
int ii = 3; // ii 是左值,3 是右值。
int jj = ii+8; // jj 是左值,ii+8 是右值。
AA aa = getTemp(); // aa 是左值 ,getTemp()的返回值是右值(临时变量)。 二、左值引用、右值引用
C++98 中的引用很常见,就是给变量取个别名,在 C++11 中,因为增加了右值引用(rvalue refer
ence)的概念,所以 C++98 中的引用都称为了左值引用(lvalue reference)。
右值引用就是给右值取个名字。
语法:数据类型&& 变量名=右值;
示例:
#include <iostream>
using namespace std;
class AA {
public:
int m_a=9;
};
AA getTemp()
{
return AA();
}
int main()
{
int&& a = 3; // 3 是右值。
int b = 8; // b 是左值。
int&& c = b + 5; // b+5 是右值。
AA&& aa = getTemp(); // getTemp()的返回值是右值(临时变量)。
cout << "a=" << a << endl;
cout << "c=" << c << endl;
cout << "aa.m_a=" << aa.m_a << endl;
}
getTemp()的返回值本来在表达式语句结束后其生命也就该终结了(因为是临时变量),而通过右值
引用重获了新生,其生命周期将与右值引用类型变量 aa 的生命周期一样,只要 aa 还活着,该右值临时
变量将会一直存活下去。
引入右值引用的主要目的是实现移动语义。
左值引用只能绑定(关联、指向)左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。
但是,常量左值引用却是个奇葩,它可以算是一个万能的引用类型,它可以绑定非常量左值、常量左
值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,
只能读不能改。
int a = 1;
const int& ra = a; // a 是非常量左值。
const int b = 1;
const int& rb = b; // b 是常量左值。
const int& rc = 1; // 1 是右值。
总结一下,其中 T 是一个具体类型:
1)左值引用, 使用 T&, 只能绑定左值。
2)右值引用, 使用 T&&, 只能绑定右值。
3)已命名的右值引用是左值。
4)常量左值,使用 const T&, 既可以绑定左值又可以绑定右值。
226、移动语义
如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。
深拷贝把对象中的堆区资源复制了一份,如果源对象(被拷贝的对象)是临时对象,拷贝完就没什么
用了,这样会造成没有意义的资源申请和释放操作。如果能够直接使用源对象拥有的资源,可以节省资源
申请和释放的时间。C++11 新增加的移动语义就能够做到这一点。
实现移动语义要增加两个函数:移动构造函数和移动赋值函数。
移动构造函数的语法:
类名(类名&& 源对象){......}
移动赋值函数的语法:
类名& operator=(类名&& 源对象){……}
注意:
1)对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移
动而不是拷贝呢?C++11 为了解决这个问题,提供了 std::move()方法来将左值转义为右值,从而方便
使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我用拷贝构造函数,用移动构造函
数吧。左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用
左值中的资源,可能会发生意想不到的错误。
2)如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函
数就去寻找拷贝构造/赋值函数。
3)C++11 中的所有容器都实现了移动语义,避免对含有资源的对象发生无谓的拷贝。
4)移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有
意义。
示例:
#include <iostream>
using namespace std;
class AA
{
public:
int* m_data = nullptr; // 数据成员,指向堆区资源的指针。
AA() = default; // 启用默认构造函数。
void alloc() { // 给数据成员 m_data 分配内存。
m_data = new int; // 分配内存。
memset(m_data, 0, sizeof(int)); // 初始化已分配的内存。
}
AA(const AA& a) { // 拷贝构造函数。
cout << "调用了拷贝构造函数。\n"; // 显示自己被调用的日志。
if (m_data == nullptr) alloc(); // 如果没有分配内存,就分配。
memcpy(m_data, a.m_data, sizeof(int)); // 把数据从源对象中拷贝过来。
}
AA(AA&& a) { // 移动构造函数。
cout << "调用了移动构造函数。\n"; // 显示自己被调用的日志。
if (m_data != nullptr) delete m_data; // 如果已分配内存,先释放掉。
m_data = a.m_data; // 把资源从源对象中转移过
来。
a.m_data = nullptr; // 把源对象中的指针置空。
}
AA& operator=(const AA& a) { // 赋值函数。
cout << "调用了赋值函数。\n"; // 显示自己被调用的日志。
if (this == &a) return *this; // 避免自我赋值。
if (m_data == nullptr) alloc(); // 如果没有分配内存,就分配。
memcpy(m_data, a.m_data, sizeof(int)); // 把数据从源对象中拷贝过来。
return *this;
}
AA& operator=(AA&& a) { // 移动赋值函数。
cout << "调用了移动赋值函数。\n"; // 显示自己被调用的日志。
if (this == &a) return *this; // 避免自我赋值。
if (m_data != nullptr) delete m_data; // 如果已分配内存,先释放掉。
m_data = a.m_data; // 把资源从源对象中转移过
来。
a.m_data = nullptr; // 把源对象中的指针置空。
return *this;
} ~AA() { // 析构函数。
if (m_data != nullptr) {
delete m_data; m_data = nullptr;
}
}
};
int main()
{
AA a1; // 创建对象 a1。
a1.alloc(); // 分配堆区资源。
*a1.m_data = 3; // 给堆区内存赋值。
cout << "a1.m_data=" << *a1.m_data << endl;
AA a2 = a1; // 将调用拷贝构造函数。
cout << "a2.m_data=" << *a2.m_data << endl;
AA a3;
a3 = a1; // 将调用赋值函数。
cout << "a3.m_data=" << *a3.m_data << endl;
auto f = [] { AA aa; aa.alloc(); *aa.m_data = 8; return aa; }; // 返回 AA 类对象的 lambda
函数。
AA a4 = f(); // lambda 函数返回临时对象,是右值,将调用移动构造函数。
cout << "a4.m_data=" << *a4.m_data << endl;
AA a6;
a6 = f(); // lambda 函数返回临时对象,是右值,将调用移动赋值函数。
cout << "a6.m_data=" << *a6.m_data << endl;
}
227、完美转发
在函数模板中,可以将参数“完美”的转发给其它函数。所谓完美,即不仅能准确的转发参数的值,
还能保证被转发参数的左、右值属性不变。
C++11 标准引入了右值引用和移动语义,所以,能否实现完美转发,决定了该参数在传递过程使用
的是拷贝语义还是移动语义。
为了支持完美转发,C++11 提供了以下方案:
1)如果模板中(包括类模板和函数模板)函数的参数书写成为 T&& 参数名,那么,函数既可以接
受左值引用,又可以接受右值引用。
2)提供了模板函数 std::forward<T>(参数) ,用于转发参数,如果 参数是一个右值,转发之后仍是
右值引用;如果参数是一个左值,转发之后仍是左值引用。
示例:
#include <iostream>
using namespace std;
void func1(int& ii) { // 如果参数是左值,调用此函数。
cout << "参数是左值=" << ii << endl;
}
void func1(int&& ii) { // 如果参数是右值,调用此函数。
cout << "参数是右值=" << ii << endl;
}
// 1)如果模板中(包括类模板和函数模板)函数的参数书写成为 T&& 参数名,
// 那么,函数既可以接受左值引用,又可以接受右值引用。
// 2)提供了模板函数 std::forward<T>(参数) ,用于转发参数,
// 如果参数是一个右值,转发之后仍是右值引用;如果 参数是一个左值,转发之后仍是左值引用。
template<typename TT>
void func(TT&& ii)
{
func1(forward<TT>(ii));
}
int main()
{
int ii = 3;
func(ii); // 实参是左值。
func(8); // 实参是右值。
}
 

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值