C++ primer plus学习笔记(二)——类、左值引用、右值引用、万能引用

深藏阁楼爱情的钟 2023-07-19 05:47 107阅读 0赞

C++ primer plus学习笔记(二)——类、左值引用、右值引用、万能引用

    • 引言
    • 代码风格
    • 左值与右值
    • 左值引用、const左值引用、右值引用
    • 万能引用
    • 类结构
    • 共享成员与对象成员
    • 析构函数
    • 类运算符重载
    • 友元函数
    • const对象与const方法
    • ==比较运算符
    • 构造函数
    • 拷贝构造函数
    • 移动构造函数
    • 赋值运算
    • 强制转化重载
    • 关闭隐式转化与自动生成函数

引言

C++也是支持面向对象的语言,也有类的概念。与java差异比较大的是,没有gc(垃圾回收器),所以设计要考虑好释放,不过也提供了智能指针(自动计数引用,自动释放)。

代码风格

前面去了解vector的源码,发现大部分头文件都会使用这种方式来避免被重复导入代码

  1. #ifndef XXX_H
  2. #define XXX_H 1
  3. code...
  4. #endif

一般头文件都会这样用文件名做一个宏定义的预处理,判断是否已经被加载过这个头文件,因为宏定义是在编译阶段处理

左值与右值

左值:能出现在赋值运算左边的值
右值:只能出现在赋值运算右边的值
根据定义,左值主要是变量、常变量(const变量),而右值包括:常量、匿名对象、函数返回值

左值引用、const左值引用、右值引用

引用类型的变量的核心是其可以修改自己对应的内存空间到别的变量(修改this)而不是简单的拷贝备份
左值引用时在类型定义的右边加上&符合的引用变量,如

  1. int a = 1;
  2. int & b = a;
  3. b = 5;//a == 5 为true

左值引用的变量会直接引用赋值变量,代表相同对象,但是不能引用右值,这样就会调用拷贝构造函数去复制
const左值引用就是通过const限定,允许左值引用引用右值,当是引用右值时,其会调用构造函数,生成一个临时变量存储右值,再去引用这个临时变量,这是为了避免直接使用普通变量存储时,需要执行一遍构造生成匿名对象,再执行一遍拷贝构造对变量初始化。因为被const限定,所以也无法修改,如

  1. const int & a = 5;

而右值引用专门用来引用右值,并且没有const的修饰,所以可以进行修改,比如

  1. int && a = 5;
  2. a = 4;//OK

所以右值引用一般代表为临时变量/对象续命,将其转移到新的容器里去生存,所以一般也要先将旧引用的一些关联置空,因为他的成员已经由新引用接管了,避免旧引用去析构被接管的成员,造成后续右值引用在释放时重复析构。事实上其与const左值引用引用右值的区别就是,其可以对临时变量进行修改,这里推荐qicosmos(江南)的这篇博客,写的非常nice——从4行代码看右值引用

万能引用

虽然有了右值引用可以引用并修改右值,但是有时候我们传入的可能是左值,也可能是右值,所以当使用泛型的右值引用来接收时,会自动根据入值是左值还是右值,来自动转化为使用左值引用还是右值引用,这种泛型右值引用也因此被叫为万能引用。如

  1. template <typename T> void forwardValue(T&& val){
  2. process(val);
  3. }
  4. template <typename T> void process(T & lVal){ }
  5. template <typename T> void process(T && rVal){ }
  6. void main(){
  7. forwardValue(1);//传入右值,val解释为右值引用
  8. int a = 1;
  9. forwardValue(a);//传入左值,val解释为左值引用
  10. }

但是因为val是具名变量,是左值,所以无论解释为何种引用,process都走的是process(T & lVal)这个左值引用的方法。这时要使用变量原有的类型作为引用传递,需要通过std::forward来实现,std::forward函数使用变量自身的引用类型作为值去传递(底层使用static_cast强制转化),此时就会分别调用右值的process和左值的process

  1. template <typename T> void forwardValue(T&& val){
  2. process(std::forward<T>(val));
  3. }

类结构

与java类似,c++也是有private、public、protected等访问权限控制符,不过没有default。然后比较大区别的是,c++默认不写的访问权限是private,java是default。还有就是,C++的类没有访问权限修饰符,把对父类的访问权限放到了子类的继承方式上。同时,其成员时按照权限写到对应权限的标签后,而不是一个个控制权限。比如

  1. class father{
  2. int a;//private default
  3. public:
  4. int b;
  5. int c;
  6. protected:
  7. int d;
  8. }
  9. class son:public father{ //extends the public authority of super class
  10. }

从这里也看出,cpp的风格和理念和java其实相差很大,java偏向于在父类限制了程序员能够派生的子类的权限,而cpp是父类不做扩展限制,只做成员的权限划分,由派生类去决定自己要扩展到多高的访问权限。这可能也和公司有关系,java的公司oracle毕竟是搞商业的(还是要恰饭的),包括一些jdk都是收费的,所以有些高权限的代码也不想给扩展和看到。java更像工具,本身自带平台,处理好了很多东西,开发只需要关注jvm,以及他提供的给你扩展的东西,在对方为你准备好的、限制好的环境下开发,出来的东西就相对安全(bug少)。而cpp就像裸权限给你,你自己随便玩,所以很容易玩坏了(出bug)。

共享成员与对象成员

共享成员是一个类被全部对象共享的成员,即全局的、静态的、非对象独有的。像是java一样,cpp也有静态成员,但是这里用了共享成员,因为他还有另一种替代品——枚举。如下代码,是使用static和通过枚举定义类里面的静态成员的两种方式。同时,cpp限制了static成员,如果要在定义时初始化,则必须是const修饰的。

  1. class Yyt{
  2. public:
  3. static int publicFuckStaticInt;//不能进行赋值初始化,要赋值的static成员必须是const
  4. enum{ cout=1};//定义枚举,共享成员的一个解决方法
  5. }
  6. int Yyt::publicFuckStaticInt = 5;//外部赋值

析构函数

cpp中因为要手动释放对象内存,所以提供了析构函数。当类的对象被释放时,会先执行析构函数,再释放内存,析构函数没有返回值,方法名是 ~类名,如下

  1. class Yyt{
  2. public:
  3. ~Yyt();//析构函数的声明
  4. }
  5. Yyt::~Yyt(){
  6. //一般cpp程序都不直接在类里面写实现,为了头文件比较清晰
  7. //一般在头文件的类里面写没实现代码的成员方法声明,然后加载另一个源代码文件
  8. //在另一个源代码文件写对应方法的实现。
  9. }

类运算符重载

cpp比较强大的是可以重载类与别的对象进行运算时,运算符的解释,将其解释为方法调用。比如重载+法

  1. class Yyt{
  2. private:
  3. int b = 3;
  4. public:
  5. int operator+(int a);
  6. }
  7. int Yyt::int operator+(int a){
  8. return b+a;
  9. }
  10. .....
  11. void main(){
  12. Yyt y;
  13. int t = y+1;//-----4
  14. //解释为y.operator+(1)
  15. }

调用类内部的重载运算符方法,要求对象必须在运算符左侧,所以一般为了实现双向的运算,会写多一个普通方法或者友元函数来处理,比如第一种方式,写多一个普通的重载运算符方法

  1. int operator+(int a,Yyt y){
  2. return y+a;
  3. }

按照参数顺序匹配,则a+y会被解释为普通重载函数 operator+(a,y),最终返回 y+a的值,另一种方法是使用友元函数

友元函数

上面讲到我们可以通过写一个普通的重载运算符函数来逆转加法的顺序,使得其走类里面的重载运算符函数。那如果不通过这种方式通过普通方式呢?这里有个问题,我们的运算函数里访问了对象的私有成员b,而在普通方法里显然没有访问的权限,那这时就得使用friend关键字,使得该方法具备权限

  1. class Yyt{
  2. private:
  3. int b = 3;
  4. .....
  5. public:
  6. friend int operator+(int a,Yyt y);
  7. }
  8. int operator+(int a,Yyt y){
  9. return a + y.b;//可以访问y.b,因为是友元函数
  10. }

const对象与const方法

const修饰的变量不能重新赋值,而const修饰的对象变量不能调用其对象方法中的非const方法。
const方法定义如下,在方法右括号后面跟上const关键字。const方法中,不能修改对象成员(即非static成员),同时不能调用this的非const方法,const修饰的方法可以理解为,不会对对象造成破坏。如:

  1. class Yyt{
  2. public:
  3. void show();
  4. void safeShow() const;
  5. }
  6. void Yyt::show(){ }
  7. void Yyt::safeShow(){
  8. //this->show();
  9. //error,不能调用上方代码,当前在const方法里,show不是const方法
  10. }
  11. void main(){
  12. const Yyt y;
  13. //y.show();
  14. //error,不能调用上方代码,当前对象被const修饰,show不是const方法
  15. y.safeShow();//OK
  16. }

==比较运算符

前面讲过赋值实际默认是走拷贝构造,当时这时使用如下代码,比较两个对象,发现结果为true

  1. class Yyt{
  2. public:
  3. operator int() const;
  4. Yyt(const Yyt & another);
  5. }
  6. Yyt::Yyt(const Yyt & another){
  7. //方法中this代表当前浅拷贝生成的新对象
  8. }
  9. Yyt::operator int() const{
  10. return 1;
  11. }
  12. void main(){
  13. Yyt y1();
  14. Yyt y2 = y1;
  15. y1 == y2? // true
  16. &y1 == &y2? // false
  17. }

实际上取地址运算后,显示两个对象不是同个地址,这是因为cpp里与java不一样,默认的 == 比较对象时,不是比较引用地址,而是调用类重载的运算符方法,如果没有提供,自动转化为可以转化的类型进行比较,没有提供类型转化则无法编译,所以这里的 y1 == y2被解释为 y1.int() == y2.int() ,也就是 1 == 1,所以为true,cpp默认不为类生成==方法,所以一般要提供这种方法,比如像java一样,比较是否同一对象,我们可以用&
运算来判断地址是否相同:

  1. class Yyt{
  2. public:
  3. bool operator==(const Yyt & another);
  4. }
  5. bool Yyt::operator==(const Yyt & another){
  6. return this == &another;
  7. }
  8. void main(){
  9. Yyt y1,y2;
  10. y1 == y2? // false
  11. }

构造函数

与java类似的,cpp的构造函数默认也会调用父类的无参构造函数,同时支持对形参直接自动转化构造,如

  1. class Yyt{
  2. private:
  3. int b = 3;
  4. public:
  5. Yyt(int c);
  6. Yyt(int c,int d);
  7. Yyt(int c,int d,int e);
  8. }
  9. Yyt::Yyt(int c){
  10. this->b = c;
  11. }
  12. Yyt::Yyt(int c,int d){
  13. this->b = c+d;
  14. }
  15. Yyt::Yyt(int c,int d,int e):b(c){
  16. //等价于 this->b = c
  17. }
  18. void main(){
  19. Yyt y1(1);
  20. Yyt * yPointer = new Yyt(1);
  21. Yyt(1);//构造匿名对象
  22. Yyt cpyY = Yyt(1);//构造匿名对象+拷贝构造函数
  23. Yyt autoConvertY = 1;
  24. Yyt doubleArgAutoConvertY = { 1,4};//b == 5
  25. }

以上罗列了几种构造函数的使用方式,请注意其中的

  1. Yyt cpyY = Yyt(1);//匿名对象+拷贝构造函数

标记不止是用了构造函数还使用了拷贝构造函数

拷贝构造函数

与java不一样的是,当使用左值的对象赋值给引用进行初始化时,实际会进行浅拷贝,而不是同个对象,比如刚刚的

  1. Yyt a = Yyt(1);//构造函数
  2. Yyt cpyY = a;//拷贝构造函数,其实也就是构造函数自动转化的特殊情况,被转为 Yyt(a)

拷贝构造函数默认浅拷贝了一个对象cpyY
拷贝构造函数的定义如下,类似构造函数,而形参是一个同类对象的const左值引用,在没有重写时,默认会有一个自动生成的拷贝构造函数,对所有对象成员进行浅拷贝。这里我们先不探究左值引用这些问题,其与普通的Yyt another区别在于,普通的方法形参接收对象,实际也会走浅拷贝,而通过引用的方式,则不会,直接传递真实对象

  1. class Yyt{
  2. public:
  3. Yyt(const Yyt & another);
  4. }
  5. Yyt::Yyt(const Yyt & another){
  6. //方法中this代表当前浅拷贝生成的新对象
  7. }

虽然这里按照书里说法,是Yyt(1)就已经生成匿名对象,因为是个右值,而a是左值,赋值给a进行初始化应该会再执行拷贝构造,但是实际测试这种使用右值来初始化的,并不会调用拷贝构造,使用了-O0和#pragma optimize(“”,off)关闭了代码优化仍是这个结果,不知道是不是c++11的标准是这样

移动构造函数

除了拷贝构造c++11中新增了移动构造函数,用来对右值进行接管构造,而不用拷贝(比如像容器的扩容操作),示例

  1. #include<iostream>
  2. class SuperClass{
  3. public:
  4. int * p;
  5. SuperClass(int _i):p(new int(_i)){
  6. //构造函数
  7. }
  8. SuperClass(const & SuperClass another):p(new int(*another.p)){
  9. //拷贝构造函数
  10. //因为是const,无法接管,只能深拷贝
  11. //如果把p接管,无法将another的p置空(const修饰),这样在析构会导致p被释放两次而报错
  12. }
  13. SuperClass(SuperClass && another):p(another.p) noexcept{
  14. //移动构造函数,将another的成员聚合到当前对象(接管过来)
  15. //然后将原有对象的成员置空(防止析构delete)
  16. another.p = nullptr;
  17. }
  18. ~SuperClass(){
  19. delete p;
  20. }
  21. };
  22. int main(){
  23. SuperClass test = std::move(SuperClass(1));
  24. return 0;
  25. }

赋值运算

前面提到的拷贝构造是在使用已经初始化的对象赋值给引用进行初始化时,那如果对已经初始化完成的引用进行对象赋值呢?实际当你没有重写赋值运算时,也是走拷贝构造,因为默认生成的赋值运算符执行拷贝构造函数,所以区分走拷贝构造还是走赋值运算就是看,赋值的引用是否已经完成初始化,因为完成初始化了,则是走对象方法,则可以进入赋值运算

  1. class Yyt{
  2. public:
  3. Yyt(const Yyt & another);
  4. Yyt operator=(const Yyt & another);
  5. }
  6. Yyt Yyt::operator=(const Yyt & another){
  7. return Yyt(another);
  8. }
  9. void main(){
  10. Yyt y1,y2;
  11. y1 = y2;//被转化为 y1.operator=(y2);
  12. //所以实际上不是赋值运算,因为其没有改变y1引用的对象
  13. //要改变引用对象可以通过指针
  14. Yyt * y3 = new Yyt();
  15. Yyt * y4 = new Yyt();
  16. delete y3;//防止内存泄漏
  17. y3 = y4;//指针赋值,y3指向了y4的地址代表的对象
  18. }

强制转化重载

除了对运算符进行函数重载,cpp还支持对强制类型转换进行重载,如

  1. class Yyt{
  2. private:
  3. int b = 3;
  4. public:
  5. operator int() const;
  6. }
  7. Yyt::operator int() const{
  8. return b;
  9. }
  10. void main(){
  11. const Yyt y;
  12. int a = y;//被解释为 y.int()---3
  13. int c = (int)y;//显示调用对应的转化类型,避免当有多个重载冲突
  14. }

关闭隐式转化与自动生成函数

前面如 类型转化,不同类型数据进行初始化引用执行构造函数自动转化都是会默认自动隐式转化,也就是说可以这样写

  1. class Yyt{
  2. private:
  3. int b = 3;
  4. public:
  5. operator int() const;
  6. Yyt(const int & _b);
  7. }
  8. Yyt::operator int() const{
  9. return b;
  10. }
  11. Yyt::Yyt(const int & _b){
  12. this->b = _b;
  13. }
  14. void main(){
  15. Yyt y = 3;//3被自动转化为Yyt(3)
  16. int a = y;//y被自动转化为 y.int(),因为只有一种匹配---3
  17. }

这种隐式转化有时可能会带来一些麻烦,我们可以通过explicit关键字关闭这种隐式转化,只需要在方法前面加上该关键字,则不再自动转化,而要通过显式声明,如:

  1. class Yyt{
  2. private:
  3. int b = 3;
  4. public:
  5. explicit operator int() const;
  6. explicit Yyt(const int & _b);
  7. }
  8. Yyt::operator int() const{
  9. return b;
  10. }
  11. Yyt::Yyt(const int & _b){
  12. this->b = _b;
  13. }
  14. void main(){
  15. Yyt y = Yyt(3);//必须显式调用
  16. int a = (int)y;//必须显式调用指定类型的强制转化
  17. }

而对于前面说到的,会自动生成的函数,可以通过重写赋值为delete删除对应方法,比如

  1. class Yyt{
  2. public:
  3. Yyt() = delete;//删除自动生成的默认构造函数
  4. Yyt & operator=(const Yyt & another) = delete;//删除默认生成的赋值运算
  5. Yyt(const Yyt & another) = delete;//删除默认生成的拷贝构造函数
  6. ~Yyt() = delete;//删除默认生成的析构函数
  7. Yyt * operator&(const Yyt & another) = delete;//删除默认生成的赋值运算
  8. }
  9. Yyt * operator & (Yyt object) = delete;//删除自动生成的取地址运算

更多文章,请搜索公众号歪歪梯Club
更多资料,请搜索公众号编程宝可梦

发表评论

表情:
评论列表 (有 0 条评论,107人围观)

还没有评论,来说两句吧...

相关阅读