C++语法补充

如果一个成员函数后面跟了一个override关键字,说明这个函数将重写这个函数

继承父类但是必须重写父类的一个函数的时候使用这个声明,否则报错。

后面加override算是一种声明,此函数要重写同名函数,所以如果将函数的名字写错了比如写成f00则会报错,这样也可以提醒代码阅读者这是一个重写的函数。

const修饰成员函数

一、const修饰成员函数
1.成员函数后加const称这个函数为常函数;
2.常函数不可以修改成员变量;
3.成员变量前加mutable后,即可在常函数中修改。

举例如下:

class Person {
public:
    int age;
    mutable string name;
    Person(int age, string name)
    {
        this->age = age;
        this->name = name;
    }
    void func() const
    {
        //age = 10;  无法修改普通成员变量的值
        name = "张三";//经过mutable修饰即可进行修改
    }
};

void func()
{
    Person p(18,"李四");
    cout << "姓名为:" << p.name << " 年龄为:" << p.age << endl;
    p.func();
    cout << "姓名为:" << p.name << " 年龄为:" << p.age << endl;

}

int main()
{
    func();
    return 0;
}


原理:

const修饰成员函数时是修饰成员函数的this指针所指向的对象,也就是保证在调用这个const成员函数的对象时不会被改变。

注意:const是修饰this指针所指向的对象

 const对象可以调用const修饰的成员函数,不能调用非cosnt修饰的成员函数。

非const对象可以调用const修饰的成员函数,和非const修饰的成员函数

虚函数

c++引入虚函数的目的

是为了允许用基类的指针来调用子类的这个函数(这样可以方便实现设计模式中的依赖倒置原则,方便多态的实现)

定义一个函数为纯虚函数为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。在基类中实现纯虚函数的方法是在函数原型后加“=0”。

可以理解为java中的interface。

编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。

纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

抽象类的介绍
抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。

(1)抽象类的定义: 称带有纯虚函数的类为抽象类。

(2)抽象类的作用:

抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。

(3)使用抽象类时注意:

• 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。

• 抽象类是不能定义对象的。
 

右值引用和std::move

说到右值,先看一下什么是右值,在c++中,一个值要么是右值,要么是左值,左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。

比如:

常见的右值:“abc",123等都是右值。

右值引用,用以引用一个右值,可以延长右值的生命期,比如:

int&& i = 123;
int&& j = std::move(i);
int&& k = i;//编译不过,这里i是一个左值,右值引用只能引用右值

可以通过下面的代码,更深入的体会左值引用和右值引用的区别:

int i;
int&& j = i++;
int&& k = ++i;
int& m = i++;
int& l = ++i;

move.cpp: In function ‘int main()’:
move.cpp:72:14: error: cannot bind ‘int’ lvalue to ‘int&&’
  int&& k = ++i;
              ^
move.cpp:73:15: error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
     int& m = i++;

为什么需要右值引用

C++引入右值引用之后,可以通过右值引用,充分使用临时变量,或者即将不使用的变量即右值的资源,减少不必要的拷贝,提高效率。如下代码,均会产生临时变量:

class RValue {
};

RValue get() {
    return RValue();
}

void put(RValue){}

为了充分利用右值的资源,减少不必要的拷贝,C++11引入了右值引用(&&),移动构造函数,移动复制运算符以及std::move。

右值引用(&&),移动构造函数,移动复制运算符以及std::move

将上面的类定义补充完整:

ut<<#include <iostream>
#include <memory>
#include <vector>
#include <string>
using namespace std;

struct RValue {
	RValue():sources("hello!!!"){}
	RValue(RValue&& a) {
		sources = std::move(a.sources);
		cout<<"&& RValue"<<endl;
	}

	RValue(const RValue& a) {
		sources = a.sources;
		cout<<"& RValue"<<endl;
	}

	void operator=(const RValue&& a) {
		sources = std::move(a.sources);
		cout<<"&& =="<<endl;
	}

	void operator=(const RValue& a) {
		sources = a.sources;
		cout<<"& =="<<endl;
	}

	string sources;;
};

RValue get() {
        RValue a;
	return a;
}

void put(RValue){}

int main() {
	RValue a = get();
        cout<<"---------------"<<endl;
	put(RValue());
	return 0;
}

不过,当运行的时候却发现没有任何输出

g++ move.cpp -std=c++11 -o move
 ./move 

这是因为,编译器做了优化,编译的时候加上-fno-elide-constructors,去掉优化

g++ move.cpp -std=c++11 -fno-elide-constructors -o move
 ./move 
&& RValue
&& RValue
---------------
&& RValue

通过上面的代码,可以看出,在没有加-fno-elide-constructors选项时,编译器做了优化,没有临时变量的生成。在加了-fno-elide-constructors选项时,get产生了两次临时变量,二put生成了一次临时变量。

将get函数稍微修改一下:

RValue get() {
	RValue a;
	return std::move(RValue());
}

g++ move.cpp -std=c++11 -o move
 ./move 
&& RValue
---------------

//加编译选项
g++ move.cpp -std=c++11 -fno-elide-constructors -o move
 ./move 
&& RValue
&& RValue
---------------
&& RValue

只是简单的修改了一下,std::move(a),在编译器做了优化的情况下,用了std::move,反而多做了一次拷贝。

其实,RValue如果在没有定义移动构造函数,重复上面的操作,生成临时变量的次数还是一样的,只不过,调用的时拷贝构造函数了而已。

通过get函数可以知道,乱用std::move在编译器开启构造函数优化的场景下反而增加了不必要的拷贝。那么,std::move应该在什么场景下使用?

std::move使用场景

1、移动构造函数的原理

通过移动构造,b指向a的资源,a不再拥有资源,这里的资源,可以是动态申请的内存,网络链接,打开的文件,也可以是本例中的string。这时候访问a的行为时未定义的,比如,如果资源是动态内存,a被移动之后,再次访问a的资源,根据移动构造函数的定义,可能是空指针,如果是资源上文的string,移动之后,a的资源为空字符串(string被移动之后,为空字符串)。

可以通过下面代码验证,修改main函数:

int main() {
	RValue a, b;
	RValue a1 = std::move(a);
	cout<<"a.sources:"<<a.sources<<endl;
	cout<<"a1.sources:"<<a1.sources<<endl;
	RValue b1(b);
	cout<<"b.sources:"<<b.sources<<endl;
	cout<<"b1.sources:"<<a1.sources<<endl;
	return 0;
}

g++ move.cpp -std=c++11 -o move
 ./move 
&& RValue
a.sources:
a1.sources:hello!!!
& RValue
b.sources:hello!!!
b1.sources:hello!!!

通过移动构造函数之后,a的资源为空,b指向了a的资源。通过拷贝构造函数,b复制了a的资源。

2、std::move的原理

std::move的定义:

这里,T&&是通用引用,需要注意和右值引用(比如int&&)区分。通过move定义可以看出,move并没有”移动“什么内容,只是将传入的值转换为右值,此外没有其他动作。std::move+移动构造函数或者移动赋值运算符,才能充分起到减少不必要拷贝的意义。

3、std::move的使用场景

在之前的项目中看到有的同事到处使用std::move,好像觉得使用了std::move就能移动资源,提升性能一样,在我看来,std::move主要使用在以下场景:

  • 使用前提:1 定义的类使用了资源并定义了移动构造函数和移动赋值运算符,2 该变量即将不再使用
  • 使用场景
RValue a, b;
	
//对a,b坐一系列操作之后,不再使用a,b,但需要保存到智能指针或者容器之中
unique_ptr<RValue> up(new RValue(std::move(a)));
vector<RValue*> vr;
vr.push_back(new RValue(std::move(b)));

//临时容器中保存的大量的元素需要复制到目标容器之中	
vector<RValue> vrs_temp;
vrs_temp.push_back(RValue());
vrs_temp.push_back(RValue());
vrs_temp.push_back(RValue());
vector<RValue> vrs(std::move(vrs_temp));
	
RValue c;
put(std::move(c));
  • 在没有右值引用之前,为了使用临时变量,通常定义const的左值引用,比如const string&,在有了右值引用之后,为了使用右值语义,不要把参数定义为常量左值引用,否则,传递右值时调用的时拷贝构造函数
void put(const RValue& c){
	cout<<"----------"<<endl;
	unique_ptr<RValue> up(new RValue(std::move(c)));
	cout<<"----------"<<endl;
}

RValue c;
put(std::move(c));

g++ move.cpp -std=c++11 -o move
 ./move 
----------
& RValue
----------

不使用左值常量引用:

void put(RValue c){
	cout<<"----------"<<endl;
	unique_ptr<RValue> up(new RValue(std::move(c)));
	cout<<"----------"<<endl;
}

RValue c;
put(std::move(c));

g++ move.cpp -std=c++11 -o move
 ./move 
&& RValue
----------
&& RValue
----------

这是因为,根据通用引用的定义,std::move(c)过程中,模板参数被推倒为const RValue&,因此,调用拷贝构造函数。

总结

通过简绍右值和右值引用以及std::move和移动构造函数,总结右值引用,移动构造函数和移动赋值运算符和std::move的用法和注意事项。

void **指针心得

当我们定义一个变量的时候,如果没有使用指针,那么这个变量的地址已经被确定下来,这个地址是没有办法被改变的,如果我们想要改变地址怎么办,那么在定义这个变量的时候就定义它的指针,也就是void *i;当指针被定义以后我们就可以操作这个变量的指针,因为这个变量的指针此时相当于一个新的变量,我们称之为指针变量,他是变量的一个指针,当我们定义完void *i的时候变量的指针被定义而且变为可操作的地址,我们可以通过这个地址来交换变量,那么当我们想要操作这个指针变量的地址怎么办,比如想要交换两个变量的地址,那么我们就要定义新的指向变量指针的指针也就是俗称的二级指针。这在函数传参的过程中很重要,传入变量的二级指针以后我们就可以随意操作一级指针所指向的内容,从而达到通过交换地址而改变变量内容的目的,避免了在函数块中无法持久化改变变量值的问题。(其实这一切都是为了在函数中改变那些本来被固定的变量,跳出函数块的限制)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值