[ C++ ] 为你系统梳理类和对象(万字长文)

2023年2月22日07:58:18

[ C++ ] 为你系统梳理类和对象(万字长文)


面向过程和面向对象初步认识

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
类似于洗衣服过程:
[ C++ ] 为你系统梳理类和对象(万字长文)
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
[ C++ ] 为你系统梳理类和对象(万字长文)

类是对象的抽象,是一种自定义数据类型,它用于描述一组对象的共同属性和行为。
C语言结构体只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。
类(class)是结构体的拓展,不仅能够拥有成员变量,还拥有成员函数。

类的定义

类是一种用于把数据和与这些数据相关的操作组合在一起的特殊数据类型。
类的定义包括了数据成员和函数成员,这些成员可以被用来描述类的抽象概念。例如,如果你想定义一个表示人的类,你可以把姓名、年龄和身高作为数据成员,而把计算 BMI 和打印个人信息作为函数成员。这样,你就可以创建多个人的对象,每个对象都有自己的数据和函数,并且可以使用类定义的函数来操作这些数据。
举个例子:

class Person {
public:
  // 数据成员:
  string name;
  int age;
  int height;
  
  // 函数成员(也称为方法):
  double calculateBMI();
  void printInfo();
};

这个类定义了一个叫做 Person 的类,该类包含了三个数据成员(name、age 和 height)和两个函数成员(calculateBMI 和 printInfo)。这些成员可以被用来描述人的概念,例如:

// 创建一个 Person 类的对象:
Person p;

// 使用数据成员来设置对象的属性:
p.name = "John Doe";
p.age = 30;
p.height = 180;

// 使用函数成员来操作对象的数据:
double bmi = p.calculateBMI();
p.printInfo();

在这个例子中,我们创建了一个 Person 类的对象,并通过设置它的数据成员来为这个对象赋值。然后,我们调用了两个函数成员:calculateBMI 用来计算对象的 BMI,printInfo 用来打印对象的信息。这样,我们就可以通过类的定义和对象的创建,来操作这些对象的数据。

类的两种定义方式:
1.函数成员的声明和定义放在一起
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yS784x5e-1670567516510)(https://cdn.jsdelivr.net/gh/Eidell/image/20221026193720.png)]
2.类中函数成员的声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
[ C++ ] 为你系统梳理类和对象(万字长文)

数据成员命名规则建议

// 我们看看这个函数,是不是很僵硬?
class Date
{
public:
	void Init(int year)
	{
	// 这里的year到底是成员变量,还是函数形参?
	year = year;
	}
private:
	int year;
};
// 所以一般都建议这样
class Date
{
public:
	void Init(int year)
{
	_year = year;
}
private:
	int _year;
};
// 或者这样
class Date
{
public:
	void Init(int year)
	{
		mYear = year;
	}
private:
	int mYear;
};

访问限定符

  • public:该访问说明符之后的各个成员都可以被公开访问,简单来说就是无论 类内 还是 类外 都可以访问。
  • protected:该访问说明符之后的各个成员可以被 类内、派生类或者友元的成员访问,但类外 不能访问。
  • private:该访问说明符之后的各个成员只能被 类内成员或者友元的成员访问,不能被从类外或者派生类中访问。
    对于struct,它的所有成员都是默认public。对于class,它的所有成员都是默认 private。

封装

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
C++ 类是一种用于实现封装的特殊数据类型。通过类的定义,我们可以把数据成员和函数成员组合在一起,并通过访问控制关键字(例如 public 和 private)来控制这些成员的访问权限。

类的作用域

类的作用域是指类定义的范围,也就是类的名称在程序中有效的范围。一般来说,类的作用域包括两个部分:
内部作用域:类定义的范围,也就是类定义中类名称的作用域。在类定义的范围内,类名称可以直接使用,不需要额外的前缀。例如:

class Person {
public:
  // 内部作用域:
  string name;
  int age;

  // 函数成员的定义:
  void printInfo() {
    // 函数内部的作用域:
    cout << "Name: " << name << endl;
    cout << "Age: " << age << endl;
  }

在这个例子中,类定义的范围就是类的内部作用域。在这个范围内,类的成员可以直接使用。

外部作用域:类定义以外的范围,也就是类名称在其他代码中的作用域。在类定义以外的范围内,类名称需要附加前缀才能使用。

类的实例化

类是对对象进行描述的,并不占据实际空间,只有当类实例化一个对象,该对象才占据实际空间,可以把类看作图纸,而类实例化的对象是图纸对应的建筑。
[ C++ ] 为你系统梳理类和对象(万字长文)

类的对象模型

  • 如何计算类对象的大小

问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?

我们先看下面的代码:

# include<iostream>
using namespace std;
/* 定义第一个类:12个字节 */
class C1
{
private:
    int a;// 4
    int b;// 4
    int c;// 4
};
/* 定义第二个类:12个字节 */
class C2
{
private:
    int a;// 4
    int b;// 4
    int c;// 4
    static int d;// 0
};
/* 定义第三个类:4个字节 */
class C3
{
private:
    int a;// 4
    static int b;// 0
public:
    void setA(int a)// 0
    {
        this->a = a;
    }
    int getA()// 0
    {
        return this->a;
    }
    static void add()// 0
    {
        C3::b += 100;
    }
};
/* 定义结构体:4个字节 */
struct S1
{
    int a;
    static int b;
};

int main()
{
    cout << "C1类所占字节数:" << sizeof(C1) << endl;  //12
    cout << "C2类所占字节数:" << sizeof(C2) << endl;		//12
    cout << "C3类所占字节数:" << sizeof(C3) << endl;		//4
    cout << "S1结构体所占字节数:" << sizeof(S1) << endl;		//4

    return 0;
}

从上面的结果,我们得出结论:

  1. C++中的数据成员和函数成员是分开存储的。
  2. 普通的数据成员存放在对象中,与结构体变量有相同的内存布局和字节对齐方式。
  3. 静态数据成员存储在全局(静态)区。
  4. 函数成员存储在代码区。
  • 结构体内存对齐规则

    1. 第一个成员在与结构体偏移量为0的地址处。
    2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8
    3. 结构体总大小为:最大对齐数(所有数据成员对齐数大的)的整数倍。
    4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
      体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

this指针

this 指针是 C++ 成员函数中的一个隐藏的指针参数,它指向当前对象。在类的函数成员中,this 指针可以用来访问当前对象的数据成员。例如,如果一个类 MyClass 有一个成员变量 x,你可以使用 this->x 来访问该变量。
this 指针是隐式地定义的,因此你不需要显式地声明它。例如,你可以在类的成员函数中直接使用 this 指针,如下所示:

class MyClass {
public:
    int x;

    void printX() {
        // 使用 this 指针访问 x
        std::cout << this->x << std::endl;
    }
};

注意:
在类的静态成员函数中,不能使用 this 指针。这是因为静态成员函数不属于任何特定的对象,因此不存在当前对象。

this指针的特性

this 指针是隐式地定义的,因此你不需要显式地声明它。
this 指针只能在类的成员函数中使用,不能在类的静态成员函数中使用。
this 指针指向当前对象,可以用来访问当前对象的成员变量。
this 指针可以作为参数传递给其他函数,表示当前对象的地址。
this 指针是一个常量指针,它的值不能被改变。
[ C++ ] 为你系统梳理类和对象(万字长文)


类的6个默认成员函数

如果一个类什么都不写,那么该类称为空类,编辑器会自动生成6个默认成员函数。
默认成员函数:用户没有显式定义,编辑器自动生成的成员函数。
[ C++ ] 为你系统梳理类和对象(万字长文)

构造函数

    • 概念

对于一个类,我们需要对其成员变量进行初始化,但是每一个我们都要手动初始化过于繁琐,那么,能不能能否让一个类在创建时就被编辑器自动初始化呢?

为此,我们引入了构造函数。构造函数是一个很特殊的成员函数,它的名字与类名相同,当创建一个类对象时,编辑器将自动调用该函数。保证类的每个数据成员都有一个合适的初始值。并且,该函数在类的生命周期中只会调用一次。

C++中的构造函数是一个特殊的成员函数,用于创建对象时初始化对象的成员变量。它通常与类一起使用,并且具有与类同名的函数名。构造函数的语法看起来像这样:

class MyClass {
public:
  MyClass(int x, int y) {
    // 初始化代码
  }
};

在这个例子中,MyClass是一个类,MyClass()是这个类的构造函数,它接受两个整型参数x和y。构造函数中的代码用于初始化类的成员变量。

在C++中,构造函数被自动调用,无需显式地调用它。当创建一个类的新实例时,构造函数将自动被调用,并且可以用来初始化新创建的对象。例如:
MyClass obj(10, 20);
在这个例子中,当创建obj对象时,构造函数被自动调用,并使用传递给构造函数的参数(10和20)来初始化obj的成员变量。

总的来说,构造函数是一种用于在创建对象时初始化对象的成员变量的特殊函数。它与类一起使用,并在创建类的新实例时自动调用。

  • 特性
  1. 构造函数与类具有相同的名称,并且不返回任何值。
  2. 构造函数用于在创建对象时初始化对象的成员变量。
  3. 构造函数是一种特殊的函数,在创建类的新实例时自动调用,无需显式地调用它。
  4. 构造函数可以重载,这意味着一个类可以具有多个构造函数,每个构造函数都具有不同的参数列表。
//构造函数的重载
class Date
{
	public:
		void Print();
		Date(int year = 2022, int month = 5, int day = 20)  //构造函数1
		{
			_year = year;
			_month = month;
			_day = day;
		}
		Date(int year,int month, int day)          //构造函数2
		{
			_year = year;
			_month = month;
			_day = day;
		}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date tmp;       //编译器自动调用构造函数
	Date x(2022, 5, 20);   //调用构造函数1
	return 0;
}
  • 构造函数也可以具有缺省参数,这意味着如果在创建对象时没有传递特定的参数,则使用缺省值。
  • 默认构造函数在类不初始化创建对象时会被编辑器自动调用
    • 默认构造函数分三种情况:
      • 显示定义的无参构造函数
      • 显示定义的全缺省构造函数
      • 编辑器默认生成的构造函数
        默认构造函数和普通构造函数区别
  • 默认构造函数只允许存在一个,若已经定义系统不在生成
  • 编辑器生成的默认构造函数存在一些问题。并不会初始化内置类型成员 ( 如int,char,float等 ) ,但会初始化自定义类型成员
  • 所以当一个类的成员变量全部是自定义类型,不需要自己写默认构造函数,直接用编辑器自动生成的就好。

析构函数

  • 概念

析构函数是 C++ 中特殊的一种成员函数,它会在某个对象的生命周期结束时自动调用。析构函数的名字与类名相同,但是它的名字前面带有一个波浪线 ~。例如,如果有一个类叫 MyClass,那么它的析构函数就是 ~MyClass。

析构函数通常用来做清理工作,例如释放动态分配的内存、关闭文件等等。举个例子,如果在某个类的构造函数中动态分配了一块内存,那么在析构函数中可以释放这块内存,以避免内存泄露。
下面是一个简单的析构函数的例子:

class MyClass
{
public:
    // 构造函数
    MyClass()
    {
        // 在构造函数中动态分配一块内存
        m_data = new int[1024];
    }

    // 析构函数
    ~MyClass()
    {
        // 在析构函数中释放动态分配的内存
        delete[] m_data;
    }

private:
    int *m_data;  // 用来存储动态分配的内存
};

当一个 MyClass 类的对象创建后,它的构造函数就会自动调用,用来分配内存。而当这个对象的生命周期结束时,它的析构函数也会自动调用,用来释放内存。这样就可以确保在对象的生命周期结束时总是能够及时释放动态分配的内存。

注意:如果一个类没有定义析构函数,那么编译器会自动为这个类生成一个默认的析构函数。默认的析构函数什么都不做,它只是简单地结束对象的生命周期。如果类中没有动态分配内存等资源,那么使用默认的析构函数就足够了。但是,如果类中有动态分配的内存或者其他资源,那么必须自己定义析构函数来释放这些资源,以避免内存泄露等问题。

  • 特性

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~,比如~类名()
  2. 析构函数不能带有参数,也不能返回任何值。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
  4. 和构造函数一样,当一个类中有自定义成员时,析构函数会调用这些自定义成员的析构函数。
  5. 类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,默认的析构函数什么都不做,它只是简单地结束对象的生命周期;有资源申请时,一定要写,否则会造成资源泄漏》

拷贝构造函数

  • 概念

在创建对象时,我们能否,复制出一个一模一样的对象呢?拷贝构造函数就能完成这件事。

拷贝构造函数是 C++ 中另一种特殊的构造函数,它的作用是在创建对象时,将另一个同类型的对象作为模板,复制它的数据成员到新创建的对象中。拷贝构造函数的名字与类名相同,它的语法如下:

class MyClass
{
public:
    // 拷贝构造函数
    MyClass(const MyClass &other)
    {
        // 将 other 对象中的数据成员复制到新创建的对象中
    }
};

拷贝构造函数通常用于实现深拷贝,即复制一个对象时,不仅复制它的数据成员,还要复制它所持有的所有资源,例如动态分配的内存、打开的文件等等。这样可以确保新创建的对象与原对象之间互不影响,并且在删除新对象时不会产生内存泄露等问题。

拷贝构造函数只有一个形参(不包括this),该形参是本类对象的引用,一般最好要加const修饰。当用已经存在的类对象去创建一个新类对象时,编辑器自动调用该函数。

拷贝构造只有一个参数,且必须是同类对象的引用。如果是传值参数,将会发生无限循环。因为形参是实参的一份拷贝。当把这个形参传入时,又需要拷贝构造。就这样无限循环。而且最好加const修饰,防止权限的放大。

拷贝构造函数使用形式如下:

classname a;   //创建类对象a
classname b(a); //以对象a为蓝本创建新对象b,也可以写成classname b = a;
  • 拷贝构造函数特性
  1. 拷贝构造函数的名字与类名相同,它的原型是 ClassName(const ClassName &)。
  2. 拷贝构造函数的参数是一个 const 类型的引用,表示被复制的对象。
  3. 拷贝构造函数会在创建对象时自动调用,用来复制另一个同类型的对象的数据成员到新创建的对象中。
  4. 如果一个类没有定义拷贝构造函数,那么编译器会自动为这个类生成一个默认的拷贝构造函数。默认的拷贝构造函数实现了浅拷贝,即仅复制对象的数据成员,而不复制它所持有的资源。如果需要实现深拷贝,必须自己定义拷贝构造函数。
  • 默认拷贝构造函数
    默认情况下,不需要添加拷贝构造函数,编译器会默认配置一个。默认的拷贝构造函数对于对象的拷贝是浅拷贝(值拷贝)。生成默认的拷贝构造函数对内置类型值拷贝,对自定义类型调用它的拷贝函数。
    这种拷贝存在一定问题:
    比如当类中有指针变量时,浅拷贝只会把两个指针变成相同的值,而忽略了实际意义。当这个指针指向的是一个栈时,浅拷贝只是让两个指针指向同一个栈,而没有去创建一个新的栈。

如下所示:

class Time
{
public:
		Time()    //构造函数
		{
				_hour = 1;
				_minute = 1;
				_second = 1;
		}
		Time(const Time& t)   //定义拷贝构造函数
		{
				_hour = t._hour;
				_minute = t._minute;
				_second = t._second;		
		}
}

运算符重载

运算符重载概念

运算符重载是指在 C++ 中,允许将已有的运算符(如 +、-、* 等)用于自定义数据类型(例如类或结构体)。这样,我们就可以使用已有的运算符来操作这些自定义数据类型,这样做可以提高代码的可读性和可维护性。

具体来说,运算符重载就是定义一个函数来改变运算符的含义,以适应特定的数据类型。这个函数称为重载运算符函数。

运算符重载语法格式如下:

// 定义重载运算符函数
返回类型 operator运算符(参数列表) {
  // 函数体
}

例如,如果要重载 + 运算符,可以使用如下的语法:

class Vector2D {
 public:
  double x, y;

  // 定义重载运算符函数
  Vector2D operator+(const Vector2D& v) const {
    Vector2D result;
    result.x = x + v.x;
    result.y = y + v.y;
    return result;
  }
};

除了函数名称中的operator关键字,运算符重载函数与普通函数没有区别。

运算符重载特性

  • 只能对现有的运算符进行重载,不能自行定义新的运算符。例如,一个数的幂运算,试图重载“**”为幂运算符,使用2**4表示2^4是不可行的。
  • 以下运算符不能被重载:::(作用域解析),.(成员访问),.*(通过成员指针的成员访问),?:(三目运算符)
  • 重载后的运算符,其运算优先级,运算操作数,结合方向不得改变
    [ C++ ] 为你系统梳理类和对象(万字长文)

运算符重载参数限制

非成员函数:
  单目运算符:参数表中只有一个参数
  双目运算符:参数表中只有两个参数
成员函数:
  单目运算符:参数表中没有参数
  双目运算符:参数表中只有一个参数

赋值运算符重载

赋值运算符重载是指在 C++ 中,可以定义一个函数来改变赋值运算符 = 的含义,以适应特定的数据类型。这样,我们就可以使用 = 运算符来赋值自定义的数据类型,而不用再写长长的赋值语句。

赋值运算符重载格式

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this :要复合连续赋值的含义
class Date
{
public:
		Date& operator=(const Date& d)  //引用提高效率
		{
				if(this != &d)  //检测是否自己给自己赋值
				{
						_year = d._year;
						_month = d._month;
						_day = d._day;
				}
				return *this;
		}
private:
		int  _year ;
		int  _month ;
		int  _day ;
};

特性

  1. 赋值运算符只能重载成类的成员函数不能重载成全局函数
    如下所示:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。
2. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必
须要实现。

流提取>>和流插入<<的重载

cin 和 cout 是C++标准库中的两个流对象,它们分别用于读取和写入数据。

cin是标准输入流,通常指向键盘。我们可以使用它来读取用户输入的数据,例如:

int x;
cin >> x; // 读取一个整数

cout是标准输出流,通常指向屏幕。我们可以使用它来向用户输出数据,例如:

cout << "Hello, world!" << endl; // 输出一行文本

注意的是,在使用cin和cout之前,我们需要在代码中包含iostream头文件。

cout和cin流对象支持多种类型的数据的输入和输出,这是怎样实现的呢❓

这是通过运算符重载实现的,对内置类型的输入和输出已经在库文件中写好了。所以当我们想输入和输出一个自定义类型对象,就需要自己写一个运算符重载。

假如我们现在想输出一下日期类对象,该怎么做❓
先看如下代码:

class Date
{
public:
	// cout流对象的类型为ostream
	void operator<<(ostream& out)
	{
		out << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
}
Date d1;

这样写存在问题,那就是重载运算符的左操作数是输出的对象,我们只能这么使用
d1<<cout
看起来很难受。
所以记住:<< 和 >> 重载一般不作为成员函数,因为this指针抢了第一个位置,使用起来很别扭。

如何改进❓
写成全局函数,这样可以自定义两个参数位置。
[ C++ ] 为你系统梳理类和对象(万字长文)
这又产生了另一个问题,如何访问类的私有成员❓
三种方式:

  1. 把数据成员的私有权限改为公共权限
    测试一下:
    [ C++ ] 为你系统梳理类和对象(万字长文)
    结果报错,提示重定义,为什么❓

如果定义了头文件Date.h和源文件test.cpp,Date.cpp。我们在Date.h中定义一个全局函数,在test.cpp和Date.cpp都Date.h,,这样我们就会出现重复定义的问题。
解决方法:
方法1:不要在头文件中实现函数,而将声明和实现分别写在.h和.cpp中。
方法2:如果一定要在头文件中实现函数,请在.h中实现的函数加上static。

2.设置成员函数get()来访问成员变量
3.友元声明

实际使用中我们常常会连续输出,所以要返回一个ostream类型对象
所以如下定义:

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

const成员

先创建一个日期类常量对象
const Date d2(2022,10,10);
调用打印函数
d2.Print();
结果报错,为什么❓
成员函数Print() 存在一个this指针,类型为 Date const * ,const修饰指针本身
&d2指向内容不可修改。的类型const Date * ,const修饰指向的内容。
当传递指针和引用类型时,要考虑权限问题。
显然,这里权限被放大了,this指针指向内容可以修改,&d2指向内容不可修改。

如何解决❓
用 const 修饰成员函数,实际就是修饰* this

void Print() const
{
	......
}

this指针类型变为const Date * cosnt this,指向内容就不可修变,便不存在权限问题了。

补充
const修饰成员变量:
const修饰成员变量表示成员常量,只能在初始化列表中赋值,可以被const和非const成员函数调用,但不能修改其值。
const修饰成员函数:
使用const修饰的成员函数称为常成员函数。与修饰成员变量不同的是,修饰成员函数时,const位于成员函数的后面,其格式如下:
返回值类型 函数名() const;
常成员函数特点:

  • 只能访问类的成员变量,而不能修改类的成员变量
  • 只能调用类的常成员函数,而不能调用类的非常成员函数

取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

class Date
{
	public :
	Date* operator&()
	{
		return this ;
	}
	const Date* operator&()const
	{
		return this ;
	}
	private :
		int _year ; // 年
		int _month ; // 月
		int _day ; // 日
};

再谈构造函数

构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值

如下所示:

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

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因初始化只能初始化一次,而构造函数体内可以多次赋值(不符合初始化的基本原则)。

比如:
[ C++ ] 为你系统梳理类和对象(万字长文)

总结:
构造函数函数体赋值相当于是成员变量初始化后的赋值。

初始化列表

  • 什么是初始化列表❓
    初始化列表是 C++ 中构造函数的一部分,它在构造函数体之前执行,用于初始化类的成员变量。
  • 如下所示:
class Date
{
public:
	Date(int year, int month, int day)
	: _year(year)
	, _month(month)
	, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
  • 为什么要有初始化列表❓
    使用初始化列表可以确保类的成员在构造函数体执行前被正确地初始化。
  • 如下所示:
class B
{
public:
	B()
	{
		_n = 10;
	}
private:
	const int _n;  //声明
	int _m = 1;   //缺省值
}
B b;

编译器报错
因为成员变量必须在初始化列表定义。

  • 注意
    • 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
    • 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,
      一定会先使用初始化列表初始化
    • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后
      次序无关
  • 总结
    • 初始化列表的使用场景:
      • 引用成员变量
      • const成员变量
      • 自定义类型成员(且该类没有默认构造函数时)
    • 如果没有在初始化列表中初始化:
      1. 对于内置类型,有缺省值用缺省值,没有就用随机值。
      2. 对于自定义类型,调用它的默认构造函数。
    • 关于初始化列表的经验:
      1. 尽量使用初始化列表初始化
      2. 一个类尽量提供默认构造(推荐全缺省)

????一句话概括成员变量初始化顺序
:如果成员变量有缺省值,又有初始化列表和函数体赋值,对象创建时会先走初始化列表,再走函数体赋值,最后走成员缺省值。

explicit关键字

  • 引入:
Date d1(2022);  //这是构造
Date d3(d1);  //这是拷贝构造
Date d3 = d1;  //这是拷贝构造
Date d2 = 2022;   //这是?根据2022构造一个临时对象,再由这个临时对象拷贝构造另一个对象
const Date& d5 = 2022;  //这就是隐式类型转换

如果我不想这样的转换发生,就用 explicit 关键字修饰

  • 什么样的函数支持隐式类型转换

    • 单参构造函数,Date(int year)
    • 虽然有多个参数,但是创建对象时后两个参数可以不传递。
      Date(int year, int month = 1, int day = 1)
  • 概念
    explicit 关键字是 C++ 中的一个修饰符,用于修饰构造函数。如果一个构造函数被 explicit 修饰,它就只能被显式调用,不能被隐式调用。

static成员

概念

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化(规定)

面试题:定义一个类,计算题目中创建了多少个类对象?

  • 实现代码:
class A
{
public:
    A() 
    { 
        ++_count; 
    }
    A(const A& t) 
    { 
        ++_count; 
    }
    static int GetACount() 
    { 
        return _count; 
    }
private:
    static int _count;
};
int A::_count = 0;
void TestA()
{
    cout << A::GetACount() << endl;
    A a1, a2;
    A a3(a1);
    cout << A::GetACount() << endl;
}
int main()
{
    TestA();
    return 0;
}

说明:全局变量可以在多个地方被修改,故C++尽量避免使用全局变量,
我们想让变量生命周期和全局变量一样,但作用域变小,这就出现了静态变量。
在类中的静态变量属于类,并且类的每个对象共享,但在类的外面初始化。

总结

生命周期: 变量从定义到销毁的时间范围。全局变量的生命周期是整个程序运行期间,而存放在栈区的变量则随着函数作用域结束导致出栈而销毁,除了静态变量之外的局部变量都存放于栈中。
作用域: 变量的可见代码域(块作用域,函数作用域,类作用域,程序全局作用域)。

  • 静态成员变量

    • 静态成员变量是类的一种成员,但是它不属于类的任何对象。
    • 静态成员变量存储在静态存储区,它的生命周期与整个程序相同,在程序结束时才会被销毁。
    • 静态成员变量可以通过类名和作用域解析运算符(::)来访问,而不需要创建对象。
    • 静态成员变量的初始值只能在定义时或在类的外部通过作用域解析运算符(::)来指定,不能在构造函数中初始化。
  • 问题:静态成员函数可以调用非静态成员函数吗?

不能,对于静态成员函数并不是某个对象的具体实例,也就没有this指针,无法调用非静态成员函数

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

可以,静态成员函数为所有类对象所共享

友元

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

  • 概念:

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元函数

  • 问题引入:
    现在尝试去重载operator<<输出类的成员变量,然后发现没办法将operator<<重载成成员函数。因为 cout 的输出流对象和隐含的 this 指针在抢占第一个参数的位置。所以要将 operator<< 重载成
    全局函数,以消除 this 指针。但这又会导致类外没办法访问成员(除非写get()函数),此时就需要友元来解决。

  • 使用友元函数
    友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在
    类的内部声明,声明时需要加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;
}
  • 说明:

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

问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的
输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作
数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成
全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。

友元类

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

  • 说明:
  1. 友元关系是单向的,不具有交换性
    比如下述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接
    访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行
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;
};
  • 友元关系不能传递
    如果C是B的友元, B是A的友元,则不能说明C时A的友元。
  • 友元关系不能继承,在继承位置再给大家详细介绍

内容类

  • 概念:

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限

  • 注意:

内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

  • 特性
  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系
  • 示例:
class A
{
private:
	static int k;
	int h;
public:
	class B // B天生就是A的友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;//OK
			cout << a.h << endl;//OK
		}
	};
};
int A::k = 1;
int main()
{
	A::B b;
	b.foo(A());
	return 0;
}

匿名对象

总结一下定义对象的几种方式:

// 有名对象
A aa0;
A aa1(1);
A aa2 = 2;
// 匿名对象
A();
A(3);
  • 概念:
    匿名对象是指在定义对象时不指定对象名称的对象。其生命周期只有一行。

  • 示例:

class A
{
public:
	A(int a = 0)
	:_a(a)
	{
	cout << "A(int a)" << endl;
	}
	~A()
	{
	cout << "~A()" << endl;
	}
private:
	int _a;
};
class Solution {
public:
	int Sum_Solution(int n) {
//...
	return n;
}
};
int main()
{
	A aa1;
	// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
	//A aa1();
	// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
	// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
	A();
	A aa2(2);
	// 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说
	Solution().Sum_Solution(10);
	return 0;
}

拷贝对象时的一些编译器优化

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还
是非常有用的。

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 f1(A aa)
{}
	A f2()
	{
		A aa;
		return aa;
	}
int main()
{
	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;
	// 传值返回
	f2();
	cout << endl;
	// 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);
	// 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;
	// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();
	cout << endl;
	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << endl;
	return 0;
}

再次理解类和对象

现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现
实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创
建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:

  1. 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什
    么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
  2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清
    楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、
    Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
  3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣
    机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才
    能洗衣机是什么东西。
  4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。

在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那
些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化
具体的对象

[ C++ ] 为你系统梳理类和对象(万字长文)

  • 作者:三千寒
  • 原文链接:https://blog.csdn.net/weixin_44261300/article/details/128252430
    更新时间:2023年2月22日07:58:18 ,共 16465 字。