您的位置:首页 > 编程语言 > C语言/C++

C++一些小tips记录(C++11)

2018-02-27 14:25 211 查看

一些C++的小tips

2018/02/25

struct 和 class的唯一差别是:默认成员访问说明符默认派生访问说明符

struct默认public,class默认private

派生类的作用域位于基类的作用域之内

当一个变量或者函数在派生类中无法解析时,在继续向上,在基类的作用域中查找匹配

可以通过作用域运算符
::
来访问被隐藏的变量

如果基类的析构函数不是虚函数,
delete
一个指向派生类对象的基类指针将产生未定义的行为

当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包含基类部分成员在内的整个对象

派生类进行赋值运算符时,需要显式的为基类部分赋值

对象销毁的顺序与创建的顺序相反

派生类析构函数首先执行,之后执行直接基类的析构函数

基类的构造函数首先执行,之后执行派生类的构造函数

当派生类对象被赋值给基类对象时,派生部分被切掉,因此同期和存在继承关系的类型无法兼容

2018/02/26

拷贝构造函数的应用场景

使用=的时候

将一个对象作为实参传递给非引用类型的形参

返回类型为非引用类型的函数返回对象

花括号列表初始化一个数组元素或一个聚合类成员

拷贝构造函数自己的参数类型必须是引用类型的

拷贝构造函数被用来初始化非引用类类型参数,如果其参数不是引用类型的,那么调用永远不会成功:为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又需要调用拷贝构造函数,这样无限循环

赋值运算符通常应该返回一个指向其左侧运算符对象的引用

析构函数不接受参数,不能被重载,用于释放对象在生存期分配的所有资源

隐式销毁一个内置指针类型的成员不会delete它所指向的对象

在类对象离开作用域时,析构函数自动调用。当指向一个对象的引用或指针离开作用域时,析构函数不会执行

析构函数体自身并不直接销毁成员,成员是在析构函数体之后的隐含的析构阶段被销毁的,析构函数体是作为成员销毁步骤之外的另外一部分而进行的。在析构函数体执行完成后,成员自动销毁

如果一个类需要自定义析构函数,那么也需要自定义拷贝赋值运算符和拷贝构造函数

特别是在使用new方法管理内存的时候

在不需要定义拷贝构造函数和赋值运算符时,为了阻止拷贝,可以定义为删除的函数
delete
(C++11),旧标准可以将函数定义为不可访问即
private


default
只能对能够默认合成的函数(默认构造函数、拷贝控制函数)使用,
delete
能对任何函数指定

析构函数不能是删除的成员,对于删除析构函数的类型,不能定义变量或成员,但可以动态分配对象(即指针),但不能
delete
该对象

在赋值运算符中使用swap

使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值

移动构造函数将资源从给定对象移动到正在创建的对象,使用
std::move()
函数完成,头文件
utility
,通常不使用using声明,直接使用
std::move()
,表示采用移动构造函数来完成而不是拷贝(C++11)

标准库容器、string和shared_ptr类既能支持移动也支持拷贝。IO类和unique_ptr可以移动但不能拷贝

C++11,右值引用只能绑定到一个将要销毁的对象

可以将一个右值引用绑定到要求转换的表达式、字面常量、返回右值的表达式,但不能将右值引用直接绑定到一个左值;返回非引用类型的函数,都生成右值,可以用右值引用或者const左值引用绑定;右值只能绑定到临时对象

右值引用特性

所引用的对象将要销毁

该对象没有其他用户

使用右值引用的代码可以自由的接管所引用对象的资源;右值要么是字面常量要么是表达式在求值过程中的临时对象;不能将一个右值引用绑定到一个右值引用类型的变量上。

int &&r1 = 12; //正确,字面常量是右值
int &&r2 = r1; //错误,表达式r1是左值,右值引用类型的变量
int &&r3 = std::move(r1); //正确,move可以获取绑定到左值上的右值引用,同时意味着除了对r1赋值和销毁外,不再使用它


2018/02/27

移动构造函数的第一个参数是该类类型的一个右值引用,任何额外的参数都必须有默认值

移动构造函数不分配任何新的内存,不分配资源,只是将资源移动。

class MyClass {
public:
MyClass(MyClass &&Mc) noexcept; //右值引用
};
MyClass::MyClass(MyClass &&Mc) noexcept : /*成员初始化器*/
{/*构造函数体*/}


noexcept
是C++11引入,通知标准库该函数不抛出异常

不抛出异常的移动构造函数和移动赋值运算符必须标记为
noexcept
。为了避免在移动过程中出现问题导致部分元素移动而剩余元素无法移动,出现元素缺失等问题,需要
noexcept
告诉标准库不抛出异常

赋值运算符必须能正确处理自赋值,(不能在使用右侧对象资源之前,释放左侧对象资源)

//移动赋值运算符,右值引用
MyClass &MyClass::operator=(MyClass &&rhs) noexcept {
if (this != &rhs) {
/*赋值操作*/
rhs.ptr = nullptr; //将rhs对象中指针指向nullptr
}
return *this;
}

//拷贝赋值运算符
MyClass &MyClass::operator=(MyClass &rhs) {
if (this != &rhs) {

}
return *this;
}


在移动操作后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设

当类没有自定义任何拷贝控制成员且所有非static成员都可以移动构造或赋值时,编译器才会合成移动构造函数或者移动赋值运算符

移动操作永远不会隐式定义为删除的函数

如果显式地定义
=default
,且数据成员有不可移动的,则移动操作会定义为删除的函数。

定义一个移动构造函数或移动赋值运算符的类必须定义自己的拷贝操作,否则,这些成员默认地被定义为删除的。

如果类定义了拷贝构造函数但没有定义移动构造函数,则在使用右值时,使用拷贝构造函数完成移动,赋值类似

三/五法则,如果一个类定义了任何一个拷贝操作,就应该定义所有的操作。拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数

单一的赋值运算符就可以实现拷贝赋值运算符和移动赋值运算符

class MyClass {
public:
MyClass(const MyClass &Mc) {}
MyClass(MyClass &&Mc) noexcept : /*初始化器*/ {}
MyClass &operator=(MyClass rhs) {
swap(*this, rhs);
return *this;
}
}
mc = mc2; //mc2是左值,通过拷贝构造函数
mc = std::move(mc2); //move将mc2转化为右值,使用右值构造函数


区分移动和拷贝的重载函数通常有一个版本接受一个const T&,而另一个版本接受T&&

void fun(const T&); //拷贝
void fun(T&&);      //移动


使用引用限定符&强制左侧运算对象this为一个左值

引用限定符可以是&或&&,分别指向左值或右值,引用限定符只能用于非static成员函数,且必须同时出现在声明和定义中,引用限定符必须在const后面

class Foo {
public:
Foo &operator=(const Foo&) &; //只能向可修改的左值赋值
Foo someMem() const &;
}
Foo &Foo::operation=(const Foo &rhs) & {
return *this;
}
Foo someMem() const & {

}


如果一个成员函数有引用限定符,则具有相同参数了标的所有版本都必须有引用限定符

class Foo {
public:
Foo sorted() &&;
Foo sorted() const; //错误,必须加上引用限定符
Foo sorted(std::string *); //正确,不同的参数列表
Foo sorted(std::string *) const; //正确,两个版本都没有引用限定符
};


重载运算符

- 当一个重载运算符是成员函数时,
this
绑定到左侧运算对象。成员运算符函数的显式参数数量比运算对象的数量少一个。

class Foo {
public:
//只能重载已有的运算符,不能发明运算符
Foo &operator+(Foo &f) {
elem += f.elem;
return *this;
}
private:
int elem;
}


运算符的优先级和结合律与对应的运算符保持一致

//以下等价
x == y + z;
x == (y + z);


一般而言,不应该重载逗号、取地址、逻辑与、逻辑或运算符

C++已经定义了逗号和取地址运算符在用于类类型对象的特殊含义;重载逻辑与、逻辑或会使糗事顺序无法保留

重载的运算符含义应与内置运算符含义相同或者类似

重载输入运算符必须处理输入可能失败的情况,判断输入是否成功。

class Foo {
friend ostream &operator<<(ostream &os, Foo &f);
friend istream &operator>>(istream &is, Foo &f);
public:
Foo() : elem(10), str("Foo") {} //默认初始化

~Foo() {}

private:
int elem;
string str;
};

ostream &operator<<(ostream &os, Foo &f) {
os << f.str << " " << f.elem;
return os;
}

istream &operator>>(istream &is, Foo &f) {
is >> f.elem >> f.str;
if (is) {
}
else { //如果输入失败,默认初始化
f = Foo();
}
return is;
}


下标运算符、算术运算符必须是成员函数

如果一个类包含下标运算符,则它通常会定义两个版本:一个返回普通引用,一个是类的常量成员并且返回常量引用

重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象

函数调用运算符必须是成员函数,一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。这种对象叫做函数对象

//函数对象
int operator() (int val) const {
return val;
}

Foo f;
int n = f(10); // n = 10


2018/02/28

lambda表达式是函数对象

[](int a, int b) {
return a > b;
}
等价于
class CompInt {
public:
bool operator() (int a, int b) const {
return a > b;
}
}


lambda表达式产生的类不含有默认构造函数、赋值运算符以及默认析构函数;是否含有默认拷贝/移动构造函数根据要捕获的数据成员类型而定

不同类型的可调用对象可能共享同一种调用形式

调用形式指明了调用返回的类型以及传递给调用的实参类型

函数表用户存储指向可调用对象的指针

不能直接将重载的函数的名字存入function类型的对象中,会造成二义性问题

解决二义性可以通过存储函数指针而非函数的名字;或者通过lambda表达式消除二义性

类型转换运算符是一种特殊成员函数,负责将一个类类型的值转换成其他类型

operator type() const


只要该类型type能够作为函数的返回类型,就可以进行定义,因此函数类型或者数组类型不可以

一个类类型转换函数必须是类的成员函数,没有显式的返回类型,没有形参,const成员

类型转换运算符应该谨慎使用

C++11引入显示的类型转换运算符

class SmallInt {
public:
explicit operator int() const {
return val;
}
private:
int val;
}
SmallInt si = 3;
static_cast<int>(si) + 3;


当表达式出现在下列位置时,显式的类型转换将被隐式执行:if/for/while/逻辑运算符/条件运算符

向bool的类型转换通常用在条件部分,因此operator bool 一般定义为explicit的

如果对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C++ 面向对象