【C++】类和对象


类和对象(上)

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

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。

C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

面向对象的三大特征:封装、继承、多态

C++把数据和方法封装在一起


2. 类的概念和类的定义

class className  //定义一个名叫className的类
{
	类体:由成员函数和成员变量组成
};  注意加分号

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。

概念:

  1. 类体中内容称为类的成员
  2. 类中的变量称为类的属性或成员变量
  3. 类中的函数称为类的方法或者成员函数。

类的两种定义方式:

  1. 声明和定义全部放在类体中
    需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
    在这里插入图片描述

  2. 类声明放在.h头文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加“类名::”
    :: 的名字叫”作用域解析运算符“,在下面的例子中,:: 代表了 showInfo() 函数属于类 Person
    在这里插入图片描述


3. 类的访问限定符及封装

3.1 访问限定符

类的访问限定符有:

  1. public(公有)
  2. protected(私有)
  3. private(私有)

一个访问限定符确定了一个访问权限作用域

说明:

  1. public修饰的成员在类外可以直接被访问
  2. protected和private修饰的成员在类外不能直接被访问(此protected和private是类似的)
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  4. 如果后面没有访问限定符,作用域就到} 即类结束。
  5. class的默认访问权限为private,struct默认为public(因为struct要兼容C语言)

[!Question] C++中struct和class的区别是什么?
C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。
注意:在继承和模板参数列表位置,struct和class也有区别。

3.2 封装

在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行结合,通过访问权限来隐藏对象内部的实现细节,控制哪些方法可以在类外部直接被使用。


4. 类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域解析运算符指明成员属于哪个类域。

[!attention] 命名空间和类都拥有自己的作用域,一个命名空间象征着一个作用域,一个类象征着一个作用域

  • 类作用域:在类中定义的成员(包括数据成员和成员函数)存在于类的作用域内。类的成员只能通过类的实例(或指向类类型的指针或引用)来访问。要在类的外部访问类的成员,你需要使用类名和作用域解析运算符::,或者使用类的对象和.运算符。

  • 命名空间作用域:命名空间用于防止命名冲突,提供了一个包含命名的独特区域。在命名空间中定义的任何内容(例如,变量,函数,类等)都属于该命名空间的作用域。要访问命名空间中的成员,你需要使用命名空间的名字和作用域解析运算符::

详细说明一下 :: 运算符:

  1. 类成员的访问:作用域解析运算符常常用于类中,当你需要在类外部定义或访问某个类的成员(如成员函数、成员变量)时,你需要使用 ::。例如,MyClass::myFunction()

  2. 静态成员的访问:当类的成员被声明为静态时,你可以通过类名和作用域解析运算符访问它,而不需要创建类的实例。例如,MyClass::myStaticMember

  3. 命名空间的使用:如果你想要访问某个特定命名空间中的名称,可以使用作用域解析运算符。例如,std::cout 中的 std 是命名空间,cout 是这个命名空间中的一个名称。

  4. 全局作用域的访问:在有些情况下,你可能想要明确指定一个名称位于全局作用域,这时候也可以使用 ::。例如,::globalVariable 表示的是一个位于全局作用域的变量 globalVariable,这样可以避免与在其他作用域中定义的同名变量发生混淆。

#include <iostream>
using std::cout;
using std::endl;

char str[10] = "hello1";
int main()
{
	char str[10] = "hello2";
	cout << (::str) << endl;
	cout << str << endl;
	return 0;
}

输出结果;
hello1
hello2

5. 类的实例化

用类类型创建对象的过程,称为类的实例化

类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图

类是对对象(实例)进行描述的,是一个模型(或者说图纸)一样的东西,限定了类有哪些成员,从代码逻辑上说,定义出一个类并没有分配实际的内存空间来存储它。


6. 类的对象大小的计算

类的存储方式

  • 类的实例只保存成员变量,成员函数存放在公共的代码段
#include <iostream>
using namespace std;

// 类中既有成员变量,又有成员函数
class A1 
{
public:
	void f1() {}
private:
	int _a;
};

// 类中仅有成员函数
class A2 
{
public:
	void f2() {}
};

// 类中什么都没有---空类
class A3
{

};

int main()
{
	A1 a1;
	A2 a2;
	A3 a3;
	
	cout << sizeof(A1) << " " << sizeof(a1) << endl; //输出:4 4
	cout << sizeof(A2) << " " << sizeof(a2) << endl; //输出:1 1
	cout << sizeof(A3) << " " << sizeof(a3) << endl; //输出:1 1
	return 0;
}

结论:一个类的大小,实际就是该类中”成员变量”之和,不包含成员函数大小。和结构体类似地,需要注意内存对齐。
注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。

7.3 结构体内存对齐规则

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

[!question] 问题:

  1. 结构体怎么对齐? 为什么要进行内存对齐?
  2. 如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?
  3. 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景

7. 类成员函数的this指针

  • 有这样一个日期类
class Date
{
public:
void Init(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}
void Print()
{
	cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
	int _year;int _month;int _day;};

int main()
{
	Date d1, d2;
	d1.Init(2022,1,11);
	d2.Init(2022, 1, 12);
	d1.Print();
	d2.Print();
	return 0;
}

[!question] 当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

this指针是一个指向类成员函数的调用者(类的对象)的指针。例如,当我们调用Init函数时,this指针指向调用Init函数的Date对象。

  • 可以这么理解:
    Init函数的原型实际上是:
void Init(Date* const this, int year, int month, int day)

其中this是一个指向Date对象的常量指针。

8.2 this指针的特性

  1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
  2. 能且仅能在“成员函数”的内部使用。
  3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。

[!question] 面试题:

  1. this指针存在哪里?
    this指针是一个隐含于每一个非静态成员函数中的特殊指针。它并不是对象数据成员的一部分,不占用对象存储空间,也就是说,在类的存储空间布局中并没有this指针。当一个成员函数被调用时,系统都会把指向调用对象的指针值赋给this。在成员函数体内,可以用this指针访问调用对象的所有成员。this指针被存放在函数的栈帧中,作为函数的一个隐含参数。
  2. this指针可以为空吗?
    理论上,this指针是可以为空的。但在实际应用中,我们通常不会让this指针为空。因为this指针为空意味着当前对象不存在,这样在调用成员函数或访问成员变量时,就会出现未定义的行为,可能会导致程序崩溃。在C++中,如果一个成员函数被声明为const,那么在该成员函数中不能修改成员变量的值,也不能调用同类的非const成员函数,这是因为this指针在const成员函数中是一个指向const的指针,即const Class* const this
1.下面程序编译运行结果是? (C)
A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

2.下面程序编译运行结果是?(B)
A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
	cout<<_a<<endl;
}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->PrintA();
	return 0;
}

8.使用C++实现栈

typedef int DataType;
class Stack
{
public:
	void Init()
	{
		_array = (DataType*)malloc(sizeof(DataType) * 3);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = 3;
		_size = 0;
	}
	void Push(DataType data)
	{
		CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	void Pop()
	{
		if (Empty())
		return;
		_size--;
	}
	
	DataType Top(){ return _array[_size - 1];}
	
	int Empty() { return 0 == _size;}
	
	int Size(){ return _size;}
	
	void Destroy()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	void CheckCapacity()
	{
		if (_size == _capacity)
		{
			int newcapacity = _capacity * 2;
			DataType* temp = (DataType*)realloc(_array, newcapacity *
			sizeof(DataType));
			if (temp == NULL)
			{
				perror("realloc申请空间失败!!!");
				return;
			}
			_array = temp;
			_capacity = newcapacity;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
int main()
{
	Stack s;
	s.Init();
	s.Push(1);
	s.Push(2);
	s.Push(3);
	s.Push(4);
	printf("%d\n", s.Top());
	printf("%d\n", s.Size());
	s.Pop();
	s.Pop();
	printf("%d\n", s.Top());
	printf("%d\n", s.Size());
	s.Destroy();
	return 0;
}

类和对象(中)

9. 类的6个默认成员函数

class Date 
{
	
};

一个空类中,看似什么都没有,但是编译器会自动生成以下6个默认成员函数。
在这里插入图片描述

9.1 函数原型

class MyClass {
public:
    MyClass() {}                          // 默认构造函数
    ~MyClass() {}                         // 默认析构函数
    MyClass(const MyClass&) {}            // 拷贝构造函数
    MyClass& operator=(const MyClass&) {} // 拷贝赋值运算符
    MyClass(MyClass&&) {}                 // 移动构造函数
    MyClass& operator=(MyClass&&) {}      // 移动赋值运算符
};

10. 构造函数

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。

class Date
{
public:
	// 1.无参构造函数
	Date()
	{
		
	}
	// 2.带参构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 3.带缺省参数的构造函数
	Date(int year = 1900, int month = 1, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 3.全缺省参数的构造函数(4与1不可并存)
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

编译器生成的默认构造函数的特点:

  1. 我们不写才会生成,我们写了任意一个构造函数就不会生成了
  2. 内置类型的非自定义成员不会被初始化(C++11中,声明支持给缺省值)
  3. 自定义类型的成员会初始化,会调用这个成员的构造函数。
    总结:一般情况下我们都要自己写构造函数,决定初始化方式。但是成员变量全是自定义类型,可以考虑不写构造函数。

[!bug]
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。(否则编译器不知道调用哪个,会报错)
在这里插入图片描述
无参构造函数、全缺省构造函数、编译器默认生成的构造函数,都称为默认构造函数。

构造函数的主要任务并不是开空间创建对象,而是初始化对象

其特征如下:

  1. 函数名与类名相同。

  2. 无返回值。

  3. 对象实例化时编译器自动调用对应的构造函数。

  4. 构造函数可以重载。

  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

  6. 不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?Date的某个对象调用了编译器生成的默认构造函数,但是该对象的_year_month/_day,依旧是随机值。

[!question] 编译器生成的默认构造函数似乎并没有什么用?

答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,编译器生成默认的构造函数会对自定类型成员调用的它的默认构造函数。

C++11 中针对内置类型成员不初始化的缺陷打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

class Date
{
	private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};

11. 析构函数

对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

特性:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
  5. 如果类中没有申请资源时,析构函数可以省略不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,构造函数一定要写,否则会造成内存泄漏,比如Stack类
  6. 关于编译器自动生成的析构函数调用了自定类型成员的析构函数,测试代码如下
class Time
{
public:
	~Time()
	{
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};

int main()
{
	Date d;
	return 0;
}

// 程序运行结束后输出:~Time()

实验:静态类、局部类、全局类的析构函数调用顺序

#include <iostream>
using namespace std;

class A1
{
public:
	A1()
	{
		cout << "A1()" << endl;
	}
	~A1()
	{
		cout << "~A1()" << endl;
	}
};

class A2
{
public:
	A2()
	{
		cout << "A2()" << endl;
	}
	~A2()
	{
		cout << "~A2()" << endl;
	}
};

class A3
{
public:
	A3()
	{
		cout << "A3()" << endl;
	}
	~A3()
	{
		cout << "~A3()" << endl;
	}
};

class B1
{
public:
	B1()
	{
		cout << "B1()" << endl;
	}
	~B1()
	{
		cout << "~B1()" << endl;
	}
};

class B2
{
public:
	B2()
	{
		cout << "B2()" << endl;
	}
	~B2()
	{
		cout << "~B2()" << endl;
	}
};

class B3
{
public:
	B3()
	{
		cout << "B3()" << endl;
	}
	~B3()
	{
		cout << "~B3()" << endl;
	}
};

class C1
{
public:
	C1()
	{
		cout << "C1()" << endl;
	}
	~C1()
	{
		cout << "~C1()" << endl;
	}
};

class C2
{
public:
	C2()
	{
		cout << "C2()" << endl;
	}
	~C2()
	{
		cout << "~C2()" << endl;
	}
};

class C3
{
public:
	C3()
	{
		cout << "C3()" << endl;
	}
	~C3()
	{
		cout << "~C3()" << endl;
	}
};

class D1
{
public:
	D1()
	{
		cout << "D1()" << endl;
	}
	~D1()
	{
		cout << "~D1()" << endl;
	}
};

class D2
{
public:
	D2()
	{
		cout << "D2()" << endl;
	}
	~D2()
	{
		cout << "~D2()" << endl;
	}
};

class D3
{
public:
	D3()
	{
		cout << "D3()" << endl;
	}
	~D3()
	{
		cout << "~D3()" << endl;
	}
};



static D1 d1;
static D2 d2;
static D3 d3;

C1 c1;
C2 c2;
C3 c3;

int main()
{
	A1 a1;
	A2 a2;
	A3 a3;
	static B1 b1;
	static B2 b2;
	static B3 b3;
	return 0;
}

运行结果:
在这里插入图片描述


12. 拷贝构造函数

拷贝构造函数用于使用现有对象创建新对象

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

拷贝构造函数也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
    // Date(const Date& d) // 错误写法:编译报错,会引发无穷递归
    Date(const Date& d) // 正确写法
    {
    	_year = d._year;
    	_month = d._month;
    	_day = d._day;
    }
    

在这里插入图片描述

  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
    注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

  2. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,但是某些场景下,需要自己显式实现实现拷贝构造函数。有时,自行实现拷贝构造函数可能造成同一块空间被free()两遍的情况,代码如下:

// 下面的程序会崩溃。这里需要我们学习深拷贝来解决。
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	
	~Stack()
	{
		if (_array)
		{
		free(_array);
		_array = nullptr;
		_capacity = 0;
		_size = 0;
		}
	}
	
private:
	DataType *_array;
	size_t _size;
	size_t _capacity;
};

int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}

[!attention]
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。


13. .赋值运算符重载

13.1 运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .* :: sizeof ?: . 以上5个运算符不能重载。这个经常在笔试选择题中出现

13.2 赋值运算符重载

  1. 赋值运算符重载格式

    • 参数类型:const ClassName&,传递引用可以提高传参效率
    • 返回值类型:ClassName&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
    • 检测是否自己给自己赋值
    • 返回*this :要复合连续赋值的含义
  2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
    在这里插入图片描述

  3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载函数完成赋值

13.3 前置++和后置++重载

// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()
{
	_day += 1;
	return *this;
}
// 后置++:
// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this+1
// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
Date operator++(int)
{
	Date temp(*this);
	_day += 1;
	return temp;
}

14. const成员函数

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
在这里插入图片描述

[!question] 思考下面的几个问题:

  1. const对象可以调用非const成员函数吗?
  • 不可以。这是因为非const成员函数可能会修改对象的状态,而const对象被设计为不可变的,所以编译器不允许这样的操作。
  1. 非const对象可以调用const成员函数吗?
  • 可以。const成员函数不会修改对象,所以它们可以安全地在const和非const对象上调用。
  1. const成员函数内可以调用其它的非const成员函数吗?
  • 不可以。因为非const成员函数可能会改变对象,这与const成员函数的语义冲突,它应该保证不改变对象的状态。
  1. 非const成员函数内可以调用其它的const成员函数吗?
  • 可以。由于const成员函数不会改变对象,因此可以在任何成员函数内部调用。

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

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

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

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可。


附加:构造函数、拷贝构造函数、赋值运算符重载的概念辨析

[!example]

  1. 构造函数什么情况下被调用?
    构造函数在创建类的新对象时被调用,这包括:
  • 直接初始化
  • 列表初始化
  • 隐式转换(如果构造函数没有被标记为 explicit 的话)
  • 临时对象的创建

  1. 拷贝构造函数什么情况下被调用?
  • 通过现有对象初始化新对象。
  • 将对象作为值传递给函数。
  • 从函数返回对象(尽管编译器优化可能会消除这种情况的拷贝)。
  • 在初始化列表中,使用等号初始化成员。 请注意,如果没有提供自定义拷贝构造函数,编译器通常会生成一个默认的拷贝构造函数。

  1. 赋值运算符重载函数什么情况下被调用?
    赋值运算符重载函数在对象已经存在,并需要被赋予新值时被调用。这涉及到使用等号(=)将一个对象的值赋给另一个同类型的现有对象。例如,如果你有两个同类型的对象 ab,则 a = b; 会调用赋值运算符重载函数。
    总结:
    构造函数用于新对象的创建和初始化,
    拷贝构造函数用于通过现有对象创建新对象,
    赋值运算符重载函数用于给现有对象赋予新值。

类和对象(下)

16. 再谈构造函数

16.1 构造函数体赋值

Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,函数体内的语句只能称作是赋值。因为初始化只能初始化一次,而赋值可以多次赋值,显然,初始化另有其位置,这个位置是初始化列表。

附加:声明,定义,初始化,赋初值概念辨析

[!attention] 易混淆的概念:声明,定义,初始化,赋初值

  1. 声明:声明一个变量或函数,表示告诉编译器这个变量或函数的类型和名称。对于变量,声明并不分配内存。对于函数,声明告诉编译器函数的名称,返回类型和参数列表。
  2. 定义:对于变量,定义表示分配内存,但不必赋予初值。对于函数,定义是提供函数的实体或主体。在整个工程中,同一个变量只能被定义一次,但可以被声明无数次。
  3. 初始化:初始化是定义变量的过程中给它赋予一个初始值。初始化是定义和赋初值的组合过程。
  4. 赋初值:也是指为变量赋予初次的值,通常与初始化交互使用。

总的来说:初始化 = 定义 + 赋初值

16.2 初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

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

[!warning]

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时)

否则会报错:
![[Pasted image 20230728221617.png]]
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();
}
class Date
{
public:
	// 初始化列表是每个成员定义的地方 
	// 对自定义类型会调用它的默认构造函数 内置类型没给初始值就赋予随机值
	// 不管写不写,每个成员都要走初始化列表,且初始化顺序是成员变量声明的顺序
	Date(int year, int month, int day, int& i)
		: _year(year)//每个变量的定义处
		, _month(month)
		, _day(day)
		, _haha(1)
		, ref(i)
	{}
private:
	// 每个变量的声明处
	int _year = 121;
	// C++11支持给缺省值,这个值是给初始化列表的
	// 如果初始化列表没有显式给值,就用这个缺省值
	// 如果初始化列表给了值,就不用缺省值,而用初始化列表中的值
	// 缺省值就是一个备胎
	int _month;
	int _day;
	const int _haha;
	int& ref;
};

16.3 explicit关键字

explicit关键字可以将一个类的构造函数标记为“显式”,使用explicit的主要目的是防止编译器执行不希望发生的隐式类型转换。

#include <iostream>
using namespace std;
class Date
{
public:
	
	explicit Date(int year, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{
		
	}
	
	Date(const Date& d1)
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;
	}
	
	~Date()
	{
		cout << "~Date()" << endl;
	}
	
	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;
};


int main()
{
	Date d1(2022);         // 使用单参数构造函数创建 d1 对象
	Date d2 = 2023;        // 使用2023从int到Date的隐式转换创建 d2 对象,如果构造函数有explicit关键字,这行代码运行不通过。
	Date d3 = Date(2024);  // 显式地使用单参数构造函数创建临时对象,并使用拷贝构造函数初始化 d3,年份设置为 2024
	Date(2025);           // 创建一个临时 Date 对象,年份设置为 2025。对象立即析构,所以其生命周期只有一行
	Date d4 = { 2026,2,2 }; // 使用一个列表初始化,创建 d4 对象,如果构造函数有explicit关键字,这行代码运行不通过。
	Date d5 = Date(2027, 3, 3); // 显式地使用三参数构造函数创建临时对象,并使用拷贝构造函数初始化 d5
	Date(2028, 4, 4);      // 创建一个临时 Date 对象,对象立即析构,其生命周期只有一行
	return 0;
}

在这行代码中:

Date d5 = Date(2027, 3, 3);

首先会调用构造函数 Date(int year, int month = 1, int day = 1) 创建一个临时 Date 类的对象,然后使用这个临时对象调用拷贝构造函数来初始化 d5
但是,许多现代编译器都会应用返回值优化(RVO)或复制消除(copy elision)的优化技术,以避免不必要的拷贝。
也就是说,在启用编译器优化的情况下,编译器可能会直接在 d5 的存储位置构造对象,从而完全绕过拷贝。


17. Static成员

17.1 概念

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

[!question] 为什么静态成员变量一定要在类外进行初始化?
因为类的静态成员变量在所有对象之间共享,它们不属于任何特定对象的实例。所以它们在编译时必须在类外进行初始化,确保只分配一次存储空间。

class MyClass 
{
public:
	static int staticVar; // 声明静态成员变量
};

int MyClass::staticVar = 5; // 在类外初始化静态成员变量

class A
{
public:
	A() { ++_scount; }
	A(const A& t) { ++_scount; }
	~A() { --_scount; }
	static int GetACount() { return _scount; }
private:
	static int _scount;
};

int A::_scount = 0;

void TestA()
{
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);
	cout << A::GetACount() << endl;
}

17.2 特性

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

问题:

  1. 静态成员函数可以调用非静态成员函数吗?
    不可以。静态成员函数是属于类本身的,而不是属于类的任何特定对象。因为非静态成员函数通常操作类的非静态成员变量,所以需要一个类的实例来调用它们。静态成员函数在调用时没有这样的实例(也就是没有this指针),所以不能直接调用非静态成员函数。如果静态成员函数需要调用非静态成员函数,必须先创建或传入类的对象。

  2. 非静态成员函数可以调用类的静态成员函数吗?
    可以。非静态成员函数与特定的类实例相关联,因此它们可以访问类的所有成员,包括静态成员函数。静态成员函数不依赖于类的实例,所以非静态成员函数可以直接调用静态成员函数,无需特定的对象实例。


18. 友元

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

18.1 友元函数

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

#include <iostream>
using namespace std;

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修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同

18.2 友元类

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

  • 友元关系是单向的,不具有交换性。
    比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访Date类中私有的成员变量则不行。

  • 友元关系不能传递
    如果C是B的友元, B是A的友元,则不能说明C时A的友元。

  • 友元关系不能继承。

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;
};

19. 内部类

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

特性:

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

20. 匿名对象

#include <iostream>
using namespace std;

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;
}

遗留问题:

  1. 临时变量为什么具有常性
  2. 拷贝对象时的一些编译器优化
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_宁清

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值