【C++】类和对象(下)

简介: 【C++】类和对象(下)

前言

本篇文章是类和对象部分的收官之作,主要讲解初始化列表、构造函数的一些补充知识,static成员,上篇文章提到的友元函数,内部类以及如何理解封装。


欢迎大家📂收藏📂以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。

=========================================================================

GITEE相关代码:🌟fanfei_c的仓库🌟

=========================================================================


1.初始化列表

1.1引入

在学习这部分知识前,我们不妨先回忆下之前成员变量是如何初始化的?

比如:

class Date
{
public:
  Date(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};

但这样的初始化形式难免会遇到一些不能解决的类型,比如引用成员变量和const成员变量以及自定义类型成员(且该类没有默认构造函数时)。

如下图所示,该位置的意义是成员变量的声明而不是定义。


那C++中我们都知道应该是在创建对象时整体定义的。

但是每个成员变量是在什么地方定义的呢?

如果像是引用成员变量和const成员变量这种必须在定义时就初始化的变量怎么办?

这是C++引用了初始化列表这一概念。

1.2初始化列表

以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括

号中的初始值或表达式。

比如:

class Date
{
public:
  Date(int year, int month, int day)
    : _year(year)
    , _month(month)
    , _day(day)
  {}//花括号中和以前一样可以实现构造函数的功能
private:
  int _year;
  int _month;
  int _day;
};

1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。

2. 类中包含以下成员,必须放在初始化列表位置进行初始化:

  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时)

class A
{
public:
  A(int a)//需要传参的构造函数不是默认构造函数哦!
    :_a(a)
  {}
private:
  int _a;
};
class B
{
public:
  B(int a, int ref)
    :_aobj(a)//_aobj初始化为a    就不需要默认构造函数了
    , _ref(ref)//_ref初始化为ref
    , _n(10)//n初始化为10
  {}
private:
  A _aobj; // 没有默认构造函数
  int& _ref; // 引用
  const int _n; // const
};

3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

并且以前的这种写法,这个地方的缺省值是给初始化列表的。


注意:每个成员变量都会在初始化列表定义,不管你再初始化列表里写没写,未指定时,默认内置类型赋为随机值,自定义类型会去调用默认构造。


4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

请看下面的代码:

class A
{
public:
  A(int a)
    :_a1(a)
    , _a2(_a1)
  {}
  void Print() {
    cout << _a1 << " " << _a2 << endl;
  }
private:
  int _a2;
  int _a1;
};
int main() {
  A aa(1);
  aa.Print();
} 
//A.输出1 1
//B.程序崩溃
//C.编译不通过
//D.输出1 随机值

答案:D

如定义所讲,成员变量在类中的生命次序就是初始化列表中的初始化顺序,所以根据声明次序,初始化列表中先执行_a2(_a1) ,再执行_a1(a),导致在执行_a2(_a1) 这句时,_a1的值还未可知,所以导致最后的结果为D。

  • 我们建议声明和初始化列表顺序保持一致,避免出现理解问题。

那讲到这,很多同学会有疑惑,既然初始化列表这么好用,函数体初始化还有存在的必要么?

  • 有必要!

因为有些初始化或者检查(malloc、memset、perror)工作,初始化列表也不能全部搞定。

建议80%-100%初始化列表搞定,剩下配合函数体初始化使用。


1.3explicit关键字

请看下面的代码:

A aa1 = 1;

这中间的过程是怎样的呢?

实际上发生了一次隐式类型转换

编译器会先对1构造一个临时对象,然后再将该临时对象拷贝构造给aa1。


💥但是这个过程是有前提的💥

前提是由A类的单参数构造函数(只有一个参数或者是多参缺省)支持。

比如:

class A {
  A(int a)//单参数构造函数
    :_a(a)
  {}
};
class A {
  A(int a,int b=1,int c=1)// 多参缺省构造函数
    :_a(a)
        ,_b(b)
        ,_c(c)
  {}
};

如果不想让转换发生,就需要在构造函数前加explicit关键字

比如:

class A {
  explicit A(int a)
    :_a(a)
  {}
};

2.Static成员

2.1概念

声明为static的类成员称为类的静态成员:

  • 用static修饰的成员变量,称之为静态成员变量;
  • 用static修饰的成员函数,称之为静态成员函数。

静态成员变量一定要在类外进行初始化。

静态成员函数和静态成员变量,本质是受限制的全局变量和全局函数,受类域和访问限定符的限制。

2.2特性

  • 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区;
  • 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明;
  • 类静态成员即可用 类名::静态成员 或者 对象.静态成员(不代表这个静态成员在这个对象里面,因为静态成员是类公有的) 来访问;
  • 静态成员函数没有隐藏的this指针,不能访问任何非静态成员;
  • 静态成员也是类的成员,受public、protected、private 访问限定符的限制。

一个小知识:类名()这种写法叫做匿名对象,他的生命周期只在这一行,如A()。

来看两道问题:

1. 静态成员函数可以调用非静态成员函数吗?

答:不能,因为没有this指针。

2. 非静态成员函数可以调用类的静态成员函数吗?

答:可以。


3.友元

友元提供了一种突破封装的方式,有时提供了便利。

但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元分为:友元函数和友元类。

3.1友元函数

在运算符重载那一篇文章中📢樊梓慕->运算符重载,我们提到过友元函数的概念。

我们知道一般的运算符重载我们可以放到类内部实现来避免成员变量私有的问题。

但当我们想要重载流运算符时却遇到了问题,因为如果流运算符也在类内部重载的话,this指针为首个参数,这样和流运算符的使用方法又不相符,所以我们尝试将其放到全局来重载,那我们如何解决成员变量私有的问题呢?

下面的代码就反应了这种尴尬场景:

class Date
{
public:
  Date(int year, int month, int day)
    : _year(year)
    , _month(month)
    , _day(day)
  {}
  // d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
  // 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
  ostream& operator<<(ostream& _cout)
  {
    _cout << _year << "-" << _month << "-" << _day << endl;
    return _cout;
  }
private:
  int _year;
  int _month;
  int _day;
};

所以我们需要友元来解决这一问题。

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字

用法如下:

class Date
{
  friend ostream& operator<<(ostream& _cout, const Date& d);//友元的声明
  friend istream& operator>>(istream& _cin, Date& d);
public:
  Date(int year = 1900, int month = 1, int day = 1)
    : _year(year)
    , _month(month)
    , _day(day)
  {}
private:
  int _year;
  int _month;
  int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)//友元的定义
{
  _cout << d._year << "-" << d._month << "-" << d._day;
  return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
  _cin >> d._year;
  _cin >> d._month;
  _cin >> d._day;
  return _cin;
}
int main()
{
  Date d;
  cin >> d;
  cout << d << endl;
  return 0;
}

友元函数的使用需要注意以下问题:

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数;
  • 友元函数不能用const修饰;
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制一个函数可以是多个类的友元函数;
  • 友元函数的调用与普通函数的调用原理相同。

3.2友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

1.友元关系是单向的,不具有交换性

比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。

(Time说(声明)Date是我的朋友,他可以来我家玩,但是Date并不一定这么认为)

2.友元关系不能传递

如果B是A的友元,C是B的友元,则不能说明C时A的友元。

3.友元关系不能继承


4.内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。

内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员

外部类对内部类没有任何优越的访问权限。

注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。(外部类认为内部类是他的朋友,但内部类并不这么认为)

特性:

  • 内部类可以定义在外部类的public、protected、private都是可以的。
  • 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  • sizeof(外部类)=外部类,和内部类没有任何关系。

5.编译器对连续构造、拷贝构造次数的优化

较为新版的编译器会对构造、拷贝构造的次数进行优化。

也就是说他会较为智能的节省拷贝与拷贝构造的次数。

注意:本篇文章所讲的编译器优化不适用所有编译器,只是一般情况,还有一些编译器跨表达式也可以优化。

比如:

同一个表达式中(需要特别注意是在同一个表达式中)

  • 构造+构造 -> 构造
  • 构造+拷贝构造 -> 构造
  • 拷贝构造+拷贝构造 -> 拷贝构造

下面我已经构建好了一些优化的场景,大家可以学习下。

class A
{
public:
  A(int a = 0)
    :_a(a)
  {
    cout << "A(int a)" << endl;
  }
  A(const A& aa)
    :_a(aa._a)
  {
    cout << "A(const A& aa)" << endl;
  }
  A& operator=(const A& aa)
  {
    cout << "A& operator=(const A& aa)" << endl;
    if (this != &aa)
    {
      _a = aa._a;
    }
    return *this;
  }
  ~A()
  {
    cout << "~A()" << endl;
  }
private:
  int _a;
};
void func1(A aa1) {}
A func2()
{
  A aa;
  return aa;
}
int main()
{
  // 1、先用1构造一个临时对象,再用临时对象拷贝构造aa1
  A aa1 = 1;// 构造+拷贝构造->构造
  // 2、先用2构造一个临时对象,再用临时对象拷贝构造aa2
  //这个例子证明了优化是现实存在的
  //因为aa2不能直接引用常量,所以这里实际上先构造了一个临时对象,然后将该临时对象拷贝构造给aa2
  const A& aa2 = 2; // 构造+拷贝构造->构造
  //3、未在同一表达式,不优化(跨表达式在一些编译器上也会优化,但建议大家还是默认不优化)
  A aa(1);//构造
  func1(aa);//拷贝构造
  //4、先构造了一个临时对象,然后将该临时对象拷贝构造给形参aa1
  func1(A(2));//构造+拷贝构造->构造 
  //5、单参数传参支持这样写,先构造一个临时对象,再将该临时对象拷贝构造给形参aa1
  func1(3);//构造+拷贝构造->构造 
  //6、函数内部,先构造aa,然后调用拷贝构造保存aa的值,出作用域析构,将之前拷贝构造再拷贝构造给aa3
  A aa3 = func2();//拷贝构造+拷贝构造->拷贝构造
  /*********************************注意***********************************/
  A aa4(aa1);  // 拷贝构造
  A aa5 = aa1; // 拷贝构造 or 赋值拷贝
  // 两个已经存在的对象拷贝,赋值拷贝
  aa4 = aa5;
  return 0;
}

=========================================================================

如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容

🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎

🌟~ 点赞收藏+关注 ~🌟

=========================================================================

目录
相关文章
|
1天前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
1天前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
1天前
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
3天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
70 19
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
51 13
|
1月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
53 5
|
1月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
41 5
|
1月前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
48 4
|
1月前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
34 3