C++类和对象(下)

简介: 在前两篇关于类和对象的文章中,我们学习了C++类的基本形式、对象的创建与使用以及每个类中都有的六大天选之子:默认成员函数,现在对类的基本框架已经搭好,关于类和对象的学习还存在一些细节,深入理解这些细节就是本文的主要目的

✨个人主页: Yohifo

🎉所属专栏: C++修行之路

🎊每篇一句: 图片来源


I do not believe in taking the right decision. I take a decision and make it right.


我不相信什么正确的决定。我都是先做决定,然后把事情做好。


d14923f6d37e570bd0570ac7e5dca5e.png


📘前言


在前两篇关于类和对象的文章中,我们学习了C++类的基本形式、对象的创建与使用以及每个类中都有的六大天选之子:默认成员函数,现在对类的基本框架已经搭好,关于类和对象的学习还存在一些细节,深入理解这些细节就是本文的主要目的


9e4d88beb08316b21b4e9f4fe3e0052.png


📘正文


先从上篇文章中的结论开始学习

68bc95019ccdf0ac63eae0028007c0c.png



📖初始化列表


初始化列表是祖师爷认证的成员变量初始化位置,初始化列表紧跟在默认构造函数之后,形式比较奇怪:主要通过 : 、, 和 ()实现初始化


class Date
{
public:
    Date(int year = 1970, int month = 1, int day = 1)
        :_year(year)  //以下三行构成初始化列表
        , _month(month)
        , _day(day)
    {
        //………
    }
private:
    int _year;
    int _month;
    int _day;
};


学习初始化列表前先来简单回顾下原初始化方式


🖋️原初始化方式


之前我们的默认构造函数是这样的:


class Date
{
public:
    Date(int year = 1970, int month = 1, int day = 1)
    {
      //此时是赋值,变量在使用前,仍然是随机值状态
      _year = year;
      _month = month;
      _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};


实例化一个对象,调试结果如下

正式赋值前已被初始化为随机值

cbfc4b6f28e9adfcb28fbd3d988384f.png

并且如果我们的成员变量中新增一个 const 修饰的成员:


private:
    int _year;
    int _month;
    int _day;
    const int _ci


程序运行结果就会变成这样:

ba1e7eb1290c8d4caab5d2bea29cb49.png


直接编译报错了,证明当前初始化方式存在很大问题


原因:


原默认构造函数是以赋值的方式实现“初始化”的

被赋值的前提是已存在,而存在必然伴随着初始化行为

此时由编译器负责,也就是编译器的惯用手段:给变量置以随机值实现初始化

成员变量在被赋值前已经被初始化了

const 修饰的成员具有常性,只能初始化一次

也就意味着此时的成员 _ci 已经被初始化为随机值,并且被 const 修饰,具有常性,无法被赋值

总结: 原赋值的初始化方式在某些场景下行不通

原赋值初始化方式的缺点:


无法给 const 修饰成员初始化

无法给 引用 成员初始化

无法给 自定义 成员初始化(且该类没有默认构造函数时)

此时祖师爷看不下去了,决定打造一种新的初始化方式:初始化列表,并为初始化列表指定了一个特殊位置:默认构造函数之后


f09e38b0c26fa549de4f9103defd9db.png

🖋️使用初始化列表


初始化列表基本形式:


紧跟在默认构造函数之后,首先以 ; 开始

初始化格式为 待初始化对象(初始值)

之后待初始化成员都以 , 开头

不存在结尾符号,除了第一个成员用 ; 开头外,其他成员都用 , 开头

class Date
{
public:
    Date(int year = 1970, int month = 1, int day = 1, const int ci = 0, const int& ref = 0)
        :_year(year)  //以下三行构成初始化列表
        , _month(month)
        , _day(day)
        , _ci(ci) //const 成员能初始化
        , _ref(ref) //引用成员也能初始化
    {
        //………
    }
private:
    int _year;
    int _month;
    int _day;
    const int _ci;
    const int& _ref;
};


在初始化列表的加持下,程序运行结果如下

进入默认构造函数体内时,成员变量已被初始化


5fa98177e8927179b3ecb94ec8419e4.png

初始化列表能完美弥补原赋值初始化的缺点


如此好用的初始化方式为何不用呢?

祖师爷推荐: 尽量使用初始化列表进行初始化,全能又安心


强大的功能靠着周全的规则支撑,初始化列表有很多注意事项(使用规则)


🖋️注意事项


使用方式


; 开始 , 分隔,() 内写上初始值

注意


初始化列表中的成员只能出现一次

初始化列表中的初始化顺序取决类中的声明顺序

以下几种类型必须使用初始化列表进行初始化

const 修饰

引用 类型

自定义类型,且该自定义类型没有默认构造函数

建议


优先选择使用初始化列表

列表中的顺序与声明时的顺序保持一致

规范使用初始化列表,高高兴兴使用类

517b0ed27228c4a03afed58ba4327df.png


📖explicit关键字


explicit 是新的关键字,常用于修饰 默认构造函数,限制隐式转换,使得程序运行更加规范


🖋️隐式转换


所谓隐式转换就算编译器在看到赋值双方类型不匹配时,会将创建一个同类型的临时变量,将 = 左边的值拷贝给临时变量,再拷贝给 = 右边的值,比如:


int a = 10;
double b = 3.1415926;
a = b;  //成功赋值,将会截取浮点数 b 的整数部分拷贝给临时变量,再赋值给 a


具体赋值过程如下

需要借助一个同类型的临时变量

80d390723158d0e954f87ca761364a2.png

将此思想引入类中,假设存在这样一个类:


class A
{
public:
  //默认构造函数
  A(int a = 0)
  :_a(a)
  {
  //表示默认构造函数被调用过
  cout << "A(int a = 0)" << endl;
  }
  //默认析构函数
  ~A()
  {
  _a = 0;
  //表示默认析构函数已被调用
  cout << "~A" << endl;
  }
  //拷贝构造函数
  A(const A& a)
  {
  _a = a._a;
  cout << "A(const A& a)" << endl;
  }
  //赋值重载函数
  A& operator=(const A& a)
  {
  if(this != &a)
  {
    _a = a._a;
  }
  cout << "A& operator=(const A& a)" << endl;
  return *this;
  }
private:
  int _a;
};



以下语句的赋值行为是合法的


int main()
{
  A aa1 = 100;  //注:此时整型 100 能赋给自定义类型
  return 0;
}


合法原因:


类中只有一个整型成员

赋值时,会先生成同类型临时变量,即调用一次构造函数

再调用拷贝构造函数,将临时变量的值拷贝给 aa1

57295a9513c21de6bfa5acefbb3594d.png

我们可以看看打印结果是否真的如我们想的一样

c28ba1d711d43c4ca9444da082cfbdc.png

结果:只调用了一次构造函数


难道编译器偷懒了?


并不是,实际这是编译器的优化

与其先生成临时变量,再拷贝,不如直接对目标进行构造,这样可以提高效率

这是编译器的优化行为,大多数编译器都支持


看代码会形象些:


A aa1 = 100;  //你以为的
A aa1(100); //实际编译器干的,优化!

1

2

单参数类赋值时编译器有优化,那么多参数类呢?


class B
{
private:
  int _a;
  int _b;
  int _c;
}


多参数类编译器也会有优化


B bb1 = {1, 2, 3};  //你以为的
B bb1(1, 2, 3); //实际上编译器优化的


编译器是这样认为的:构造临时变量+拷贝构造不如让我直接给你构造


这是编译器针对隐式转换做出的优化行为


不难发现,这样的隐式转换虽然方便,但会影响代码的可读性和规范性,我们可以通过关键字explicit 限制隐式转换行为


🖋️限制转换


在默认构造函数前加上explicit修饰


class A
{
public:
  //默认构造函数
  //限制隐式转换行为
  explicit A(int a = 0)
  :_a(a)
  {
  //表示默认构造函数被调用过
  cout << "A(int a = 0)" << endl;
  }
private:
  int _a;
};


此时再次采用上面那种赋值方式会报错


A aa1 = 100;  //报错,此时编译器无法进行隐式类型转换,优化也就不存在了


414c04a17a28256afd6cbad6ec68170.png


何时采用 explicit 修饰?


想提高代码可读性和规范性时

c092c2c67eac990f749d173da5f3793.png

📖static修饰


static 译为静态的,修饰变量时,变量位于静态区,生命周期增长至程序运行周期


static 有很多讲究,可不敢随便乱用:


修饰普通局部变量时,使其能在全局使用

修饰全局变量时,破坏其外部链接属性

static 修饰时,只能被初始化一次

static 不能随便乱用



🖋️static在类中


类中被 static 修饰的成员称为 静态成员变量 或 静态成员函数


静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区

静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明

类静态成员可用 类名::静态成员 或者 对象.静态成员 来访问

静态成员函数没有隐藏的 this 指针,不能访问任何 非静态成员

静态成员也是类的成员,受 public 、protected 、private 访问限定符的限制

课代表简单总结:


静态成员变量必须在类外初始化(定义)

静态成员函数 失去了 this 指针,但当为 public 时,可以通过 类名::函数名直接访问

class Test
{
public:
  //Test(int val = 0, static int sVal = 0)
  //  :_val(val)
  //  , _sVal(sVal) //非法的,初始化列表也无法初始化静态成员变量
  //{}
  static void Print()
  {
  //cout << _val << endl; //非法的,没有 this 指针,无法访问对象成员
  cout << _sVal << endl;
  }
private:
  int _val;
  static int _sVal; //静态成员变量
};
int Test::_sVal = 0;  //静态成员变量必须在类外初始化(定义),需指定属于哪个类


静态成员变量只能初始化一次


静态成员函数没有 this 指针


静态成员函数是为静态成员变量而生


如此刁钻的成员变量究竟有何妙用呢?


答: 有的,存在即合理

利用静态成员变量只能初始化一次的特定,写出函数统计程序运行中调用了多少次构造函数


class Test
{
public:
  Test(int val = 0)
  :_val(val)
  {
  _sVal++;  //利用静态成员变量进行累加统计
  }
  static void Print()
  {
  cout << _sVal;
  }
private:
  int _val = 0;
  static int _sVal; //静态成员变量
};
int Test::_sVal = 0;  //静态成员变量必须在类外初始化(定义)
int main()
{
  Test T[10]; //调用十次构造函数
  //通过静态成员变量验证
  cout << "程序共调用了";
  Test::Print();
  cout << "次成员函数" << endl;
  return 0;
}


输出结果如下:

得益于 static 修饰的成员变量统计

f7dc0fc1f8c1c44cd5ceb6dd2c5e13d.png

注意:


静态成员函数 不可以调用 非静态成员变量,没有 this 指针

非静态成员函数 可以调用 静态成员变量,具有全局属性

📖匿名对象


C语言结构体支持创建匿名结构体,C++ 则支持创建匿名对象

e08689de2ac051f7774a846038395f5.png


匿名对象使用如下:


//假设存在日期类 Date
int main()
{
  Date(); //此处就是一个匿名对象
  return 0;
}


匿名对象拥有正常对象的所有功能,缺点就是生命周期极短,只有一行


//演示
Date(2023, 2, 10);  //匿名对象1 初始化
Date().Print(); //匿名对象2 调用打印函数
//注意:两个匿名对象相互独立,创建 匿名对象2 时, 匿名对象1 已被销毁


🖋️使用场景


匿名对象适合用于某些一次性场景,也适合用于优化性能


Date(2023, 2, 10).Print(); //单纯打印日期 2023 2 10


//函数返回时
Date d(2002, 1, 1);
return d;
//等价于
return Date(2002, 1, 1);  //提高效率


📖友元


新增关键字 friend ,译为朋友,常用于外部函数在类中的友好声明


类中的成员变量为私有,类外函数无法随意访问,但可以在类中将类外函数声明为友元函数,此时函数可以正常访问类中私有成员


友元函数会破坏类域的完整性,有利有弊



注意:


友元是单向关系

友元不具有传递性

友元不能继承

友元声明可以写在类中的任意位置

🖋️友元函数

friend 修饰函数时,称为友元函数


class Test
{
public:
  //声明外部函数 Print 为友元函数
  friend void Print(const Test&d);
  Test(int val = 100)
  :_val(val)
  {}
private:
  int _val;
};
void Print(const Test& d)
{
  cout << "For Friend " << d._val << endl;
}
int main()
{
  Test t;
  Print(t);
}


程序正常编译,结果如下:


8459fcbc80d152f2007e1d306961b23.png


友元函数可以用来解决外部运算符重载函数无法访问类中成员的问题,但还是不推荐这种方法


🖋️友元类


friend 修饰类时,称为友元类


class Time
{
  friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类
public:
  Time(int hour = 0, int minute = 0, int second = 0)
  : _hour(hour)
  , _minute(minute)
  , _second(second)
  {}
private:
  int _hour;
  int _minute;
  int _second;
};
class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  : _year(year)
  , _month(month)
  , _day(day)
  {}
  void SetTimeOfDate(int hour, int minute, int second)
  {
  // 直接访问时间类私有的成员变量
  _t._hour = hour;
  _t._minute = minute;
  _t._second = second;
  }
private:
  int _year;
  int _month;
  int _day;
  Time _t;
};



友元有种继承的感觉,但它不是继承,它也不支持继承


📖内部类


将类B写在类A中,B 称作 A 的内部类


class A
{
public:
  //B 称作 A 的内部类
  class B
  {
  private:
  int _b;
  }
private:
  int _a;
}


内部类天生就是外类的友元类


也就是说,B 天生就能访问 A 中的成员


🖋️特性


内部类在C++中比较少用,属于了解型知识


内部类的最大特性就是使得内部类能受到外类的访问限定符限制


内部类特点:


独立存在

天生就是外类的友元

用途:


可以利用内部类,将类隐藏,现实中比较少见

注意:


内部类跟其外类是独立存在的,计算外类大小时,是不包括内部类大小的

内部类受访问限定符的限定,假设为私有,内部类无法被直接使用

内部类天生就算外类的友元,即可以访问外类中的成员,而外类无法访问内部类

📖编译器优化


前面说过,编译器存在优化行为,这里就来深入探讨一下

把上面的代码搬下来用一下,方便观察发生了什么事情


class A
{
public:
  //默认构造函数
  A(int a = 0)
  :_a(a)
  {
  //表示默认构造函数被调用过
  cout << "A(int a = 0)" << endl;
  }
  //默认析构函数
  ~A()
  {
  _a = 0;
  //表示默认析构函数已被调用
  cout << "~A" << endl;
  }
  //拷贝构造函数
  A(const A& a)
  {
  _a = a._a;
  cout << "A(const A& a)" << endl;
  }
  //赋值重载函数
  A& operator=(const A& a)
  {
  if(this != &a)
  {
    _a = a._a;
  }
  cout << "A& operator=(const A& a)" << endl;
  return *this;
  }
private:
  int _a;
};



🖋️参数优化


在类外存在下面这些函数:


void func1(A aa)
{}
int main()
{
  func1(100);
  return 0;
}


预计调用后发生了这些事情:

构造(隐式转换) -> 拷贝构造(传参) -> 构造(创建aa接收参数)


编译器会出手优化


实际只发生了这些事情:

构造(直接把aa构造为目标值)

d7200ecb3a640999e9e6d21ef85e61c.png



🖋️返回优化



除了优化传参外,编译器还会优化返回值


A func2()
{
  return A(100);
}
int main()
{
  //func1(100);
  A a = func2();
  return 0;
}


预计调用后发生了这些事情:

构造(匿名对象的创建) -> 构造(临时变量) -> 拷贝构造(将匿名对象拷贝给临时变量) -> 拷贝构造(将临时变量拷贝给 a)


编译器会出手优化


实际只发生了这些事情:

构造(直接把函数匿名对象值看作目标值,构造除出 a)


现在可以证明:编译器会将某些非必要的步骤省略点,执行关键步骤


优化场景:


涉及拷贝构造+构造时,编译器多会出手

传值返回时,涉及多次拷贝构造,编译器也会出手

注意:


引用传参时,编译器无需优化,因为不会涉及拷贝构造

实际编码时,如果能采用匿名构造,就用匿名构造,会加速编译器的优化

接收参数时,如果分成两行(先定义、再接收),编译器无法优化,效率会降低

编译器只能在一行语句内进行优化,如果涉及多条语句,编译器也不敢擅自主张


🖋️编码技巧


下面是一些编码小技巧,可以提高程序运行效率


接收返回值对象时,尽量拷贝构造方式接收,不要赋值接收

函数返回时,尽量返回匿名对象

函数参数尽量使用 const& 参数



📖再次理解类和对象


8e26d374716ebde535b294455f640c9.png

出自:比特教育科技


📘总结

以上就是 类和对象(下)的全部内容了,我们在本文章学习了一些类和对象的小细节,比如明白了善用初始化列表的道理、懂得了友元函数的用法、了解了编译器的优化事实、最后还简单理解了类和对象与现实的关系,相信在这些细节的加持之下,对类和对象的理解能更上一层楼!


如果你觉得本文写的还不错的话,可以留下一个小小的赞👍,你的支持是我分享的最大动力!


如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正


目录
相关文章
|
14天前
|
C++
C++(十一)对象数组
本文介绍了C++中对象数组的使用方法及其注意事项。通过示例展示了如何定义和初始化对象数组,并解释了栈对象数组与堆对象数组在初始化时的区别。重点强调了构造器设计时应考虑无参构造器的重要性,以及在需要进一步初始化的情况下采用二段式初始化策略的应用场景。
|
14天前
|
存储 编译器 C++
C ++初阶:类和对象(中)
C ++初阶:类和对象(中)
|
14天前
|
C++
C++(十六)类之间转化
在C++中,类之间的转换可以通过转换构造函数和操作符函数实现。转换构造函数是一种单参数构造函数,用于将其他类型转换为本类类型。为了防止不必要的隐式转换,可以使用`explicit`关键字来禁止这种自动转换。此外,还可以通过定义`operator`函数来进行类型转换,该函数无参数且无返回值。下面展示了如何使用这两种方式实现自定义类型的相互转换,并通过示例代码说明了`explicit`关键字的作用。
|
14天前
|
存储 设计模式 编译器
C++(十三) 类的扩展
本文详细介绍了C++中类的各种扩展特性,包括类成员存储、`sizeof`操作符的应用、类成员函数的存储方式及其背后的`this`指针机制。此外,还探讨了`const`修饰符在成员变量和函数中的作用,以及如何通过`static`关键字实现类中的资源共享。文章还介绍了单例模式的设计思路,并讨论了指向类成员(数据成员和函数成员)的指针的使用方法。最后,还讲解了指向静态成员的指针的相关概念和应用示例。通过这些内容,帮助读者更好地理解和掌握C++面向对象编程的核心概念和技术细节。
|
27天前
|
存储 算法 编译器
c++--类(上)
c++--类(上)
|
1月前
|
编译器 C++
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
|
1月前
|
编译器 C++
virtual类的使用方法问题之静态和非静态函数成员在C++对象模型中存放如何解决
virtual类的使用方法问题之静态和非静态函数成员在C++对象模型中存放如何解决
|
1月前
|
编译器 C++
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
|
14天前
|
存储 C++
C++(五)String 字符串类
本文档详细介绍了C++中的`string`类,包括定义、初始化、字符串比较及数值与字符串之间的转换方法。`string`类简化了字符串处理,提供了丰富的功能如字符串查找、比较、拼接和替换等。文档通过示例代码展示了如何使用这些功能,并介绍了如何将数值转换为字符串以及反之亦然的方法。此外,还展示了如何使用`string`数组存储和遍历多个字符串。
|
23天前
|
存储 C++
C++ dll 传 string 类 问题
C++ dll 传 string 类 问题
16 0