C++:类和对象

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

1.面向过程(C语言)
C 语言是 面向过程 的, 关注 的是 过程 ,分析出求解问题的步骤,通过函数调用逐步解决问题。专注点是过程步骤。
2.面向对象(C++)
C++ 基于面向对象 的, 关注 的是 对象 ,将一件事情拆分成不同的对象,靠对象之间的交互完成。专注点是对象和对象之间的交互。

二.类的定义

1.定义
class className
{
 // 类体:由成员函数和成员变量组成
};
class 定义类的 关键字, ClassName 为类的名字, {} 中为类的主体,注意 类定义结束时后面 分号不能省
类体中内容称为 类的成员: 类中的 变量 称为 类的属性 成员变量 ; 类中的 函数 称为 类的方法 或者 成员函数
2.定义方式
类的定义一般有两种方式,一种是一起定义,一种是分开定义(引用头文件,加上类域符)。

三.类的访问限定符与封装

1.访问限定符

1. public 修饰的成员在类外可以直接被访问
2. protected private 修饰的成员在类外不能直接被访问 ( 此处 protected private 是类似的 )
3. 访问权限 作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class 的默认访问权限为 private struct public( 因为 struct 要兼容 C)
【面试题】
问题: C++ struct class 的区别是什么?
解答: C++ 需要兼容 C 语言,所以 C++ struct 可以当成结构体使用。另外 C++ struct 还可以用来定义类。和class定义类是一样的,区别是 struct 定义的类默认访问权限是 public class 定义的类默认访问权限是private。
2.封装
面向对象的三大特性:封装、继承、多态
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行 交互。

四.类的作用域

类定义了一个新的作用域 ,类的所有成员都在类的作用域中 类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
示例:
class Person
{
public:
 void PrintPersonInfo();
private:
 char _name[20];
 char _gender[3];
 int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo() //类域限定符
{
 cout << _name << " "<< _gender << " " << _age << endl;
}

五.类的实例化

用类类型创建对象的过程,称为类的实例化。
类是对对象进行描述的 ,是一个 模型 一样的东西,限定了类有哪些成员,定义出一个类 并没有分配实际 的内存空间来存储它;一个类可以实例化出多个对象, 实例化出的对象 占用实际的物理空间,存储类成员变量。
类就像图纸,实例化对象就是根据图纸建出来的房子。
class Person
{
public:
    void showInfo();
private:
    char* _name;
    char* _sex;
    int age;
}

void Test()
{
    Person man;  //对象实例化
}

六.类对象模型

1.类对象的存储方式

结论:一个类的大小,实际就是该类中 成员变量 ”之和,当然要注意内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
2.结构体内存对齐规则
1. 第一个成员在与结构体偏移量为 0 的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为 8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

对齐数 = 编译器默认的一个对齐数(8) 与 该成员大小(1)(4)的较小值,两个对齐数1和4,最大对齐数为4,_a先占位4,_ch占位1,但是总大小为最大对齐数的整数倍,故而总大小为8。

3.为什么要内存对齐

左图是内存对齐规则下的存储方式,右图是不按照对齐规则的方式。可以看到,如果不按照对齐规则的话,_a需要被读取两次,然而在对齐规则下,_a只需要被读取一次,所以对齐规则的作用就是方便读取,保证读取都只需要一次。

七.this指针

1.this指针定义
C++ 编译器给每个 非静态的成员函数 增加了一个隐藏的指针参 ,让该指针指向当前对象 ( 函数运行时调用该函数的对象 ) ,在函数体中所有 成员变量 的操作,都是通过该 指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成
2.this指针特性
1. this 指针的类型:类类型 * const ,即成员函数中,不能给 this指针赋值
2. 只能在 成员函数 ”的内部使用
3. this 指针本质上是 成员函数的形参 ,当对象调用成员函数时,将对象地址作为实参传递给 this 形参。所以对象中不存储 this 指针。
4. this 指针是 成员函数 第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递
3.this指针的两个问题
1.问:this指针存在哪里?
   答:栈。this是形参,所以和普通参数一样存在于函数调用的栈帧里面。
2.问:this指针可以为空么?
   答:可以(但最好不要),只要没有对this指针进行解引用,可以为空。但是这是不安全的行为,没有实际意义,实际操作中类内不可能不调用成员对象,所以this指针解引用是难免的。
// 下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
 void Print()
 {
 cout << "Print()" << endl;
 }
private:
 int _a;
};
int main()
{
 A* p = nullptr;
 p->Print();
 return 0;
}
上面这段代码答案是正常运行,因为p虽然是空指针,但是Print()函数是在公共代码区的,可以正常调用,而且没有对空的this指针解引用,所以可以正常运行。
// 下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{ 
public:
 void PrintA() 
 {
 cout<<_a<<endl;
 }
private:
 int _a;
};
int main()
{
 A* p = nullptr;
 p->PrintA();
 return 0;
}

而上面这段代码就会导致程序崩溃了,因为cout<<_a<<endl;本质上是this->_a,对空指针进行了解引用操作,这会导致程序崩溃。

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

如果一个类中什么成员都没有,简称为空类。空类中并不是什么都没有,任何类在什么都不写时,编译器会自动生成以下6 个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

九.构造函数

1.定义

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

2.特性
构造函数 是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务 并不是开 空间创建对象,而是初始化对象
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器 自动调用 对应的构造函数。
4. 构造函数可以重载。
class Date
{
public:
 // 1.无参构造函数
 Date()
 {}
 // 2.带参构造函数
 Date(int year, int month, int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
private:
 int _year;
 int _month;
 int _day;
};
void TestDate()
{
 Date d1; // 调用无参构造函数
 Date d2(2015, 1, 1); // 调用带参的构造函数
 // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
 // 意思就是不能写成Date d1();因为这样的话不能搞清是创建对象还是函数声明了
 Date d3();
}

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

class Date
{
public:
 /*
 // 如果用户显式定义了构造函数,编译器将不再生成
 Date(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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
 // 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生
成
 // 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
 Date d1;
 return 0;
}
当然,如果给出缺省值,也可以编译通过,如下:
Date(int year = 1, int month = 1, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
6.内置类型成员,不做处理(a.有些编译器会处理;b.C++11打了补丁,会在内置类型成员声明的时候给缺省值,使用缺省值初始化);自定义类型成员会去调用他的默认构造。
   1.一般情况下构造函数需要自己写;
   2.
   a.内置类型成员都有缺省值,且初始化符合要求;
   b.全是自定义类型的构造,且这些类型都定义了默认构造。
这里是先前手动定义了_pushst和_popst的默认构造,所以MyQueue不用定义构造
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
class Date
{
public:
	Date()  //无参构造
	{
		_year = 1900;
		_month = 1;
		 _day = 1;
 }
 Date(int year = 1900, int month = 1, int day = 1)   //全缺省构造
 {
 _year = year;
 _month = month;
 _day = day;
 }
private:
 int _year;
 int _month;
 int _day;
};
// 以下测试函数能通过编译吗?  不可以,因为存在多个默认构造
void Test()
{
 Date d1;
}

十.析构函数

1.定义
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
2.特性
1. 析构函数名是在类名前加上字符 ~
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时, C++ 编译系统系统自动调用析构函数。
5. 内置类型不做处理,自定义类型调用自己的析构函数
6.(1)一般情况下,有动态申请资源,就要显示写析构函数释放资源;(2)没有动态申请的资源,不需要写析构;(3)需要释放资源的成员都是自定义类型,不需要写析构(前提是每个自定义类型都有自己的默认析构,比如MyQueue)
~Stack()   //有动态申请资源,需要手动写析构函数,必须写
{
	cout << "~Stack()" << endl;
	free(_a);
	_a = nullptr;
    _capacity = _top = 0;
}

~Date()    //全是内置类型成员,不需要手动写析构函数,可以不写
{
	cout << "~Date()" << endl;
	_year = 0;
	_month = 0;
	_day = 0;
}

class MyQueue  //全是自定义类型成员,且各自都已经手写了自己的默认析构,可以不写
{
private:
	Stack _pushst;
	Stack _popst;
};

十一.拷贝构造函数

1.定义
拷贝构造函数 只有单个形参 ,该形参是对本 类类型对象的引用 ( 一般常用 const 修饰 ) ,在用 已存在的类类型 对象创建新对象时由编译器自动调用
2.特性
1. 拷贝构造函数 是构造函数的一个重载形式
2. 拷贝构造函数的 参数只有一个 必须是类类型对象的引用 ,使用 传值方式编译器直接报错 ,因为会引发无穷递归调用。 

yH5BAAAAAAALAAAAAAOAA4AAAIMhI+py+0Po5y02qsKADs=

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// Date(Date d) // 错误写法:编译报错,会引发无穷递归
	Date(const Date& d) // 正确写法
	{
		_year = d._year;     //this->_year = d._year  this是d2的别名,d是传进来的d1
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。C++规定:内置类型直接拷贝,自定义类型必须调用拷贝构造完成拷贝。
下面是没有手动写拷贝构造,使用的自动生成的默认构造函数,可以看到能实现拷贝:
4. 深拷贝的实现
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?日期类这样的类是没必要的,但是像下面这样的类就不能这样值拷贝了,应该深拷贝。
如果只是浅拷贝,会引发析构崩溃的问题,分析如下:
如图,在没有手动定义拷贝构造函数的情况下,st1和st2的_a是指向同一块空间上的,但这是两个对象,都需要调用析构函数,但是一块空间需要析构两次,这会导致程序崩溃。
如图,手动定义拷贝构造函数,st1和st2是在两块空间上的,这样析构的时候就不存在一块空间被析构两次的问题了。
tips:先析构st2再析构st1,完整代码如下:
class Stack
{
public:

	Stack(int capacity = 4)
	{
		cout << "Stack()" << endl;

		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}

		_capacity = capacity;
		_top = 0;
	}

	Stack(const Stack& st)
	{
		_a = (int*)malloc(sizeof(int) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
        memcpy(_a, st._a, sizeof(int) * st._top);
        _top = st._top;
        _capacity = st._capacity;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_capacity = _top = 0;
	}

private:
	int* _a = nullptr;
	int _top = 0;
	int _capacity;
};

class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;
};

int main()
{
	// 必须自己实现,实现深拷贝
	Stack st1;
	Stack st2(st1);
	return 0;
}

总结:类中如果没有涉及到申请资源,写不写拷贝构造都可以,一旦涉及到申请资源,必须写拷贝构造,否则就是浅拷贝。

5. 拷贝构造函数典型调用场景:
        使用已存在对象创建新对象
        函数参数类型为类类型对象
        函数返回值类型为类类型对象

十二.赋值运算符重载

1.运算符重载
C++ 为了增强代码的可读性引入了运算符重载 运算符重载是具有特殊函数名的函数 ,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字 operator 后面接需要重载的运算符符号
函数原型: 返回值类型  operator 操作符 ( 参数列表 )
注意:
1.不能通过连接其他符号来创建新的操作符:比如 operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的 this
5.   .*     ::      sizeof      ?:     .    注意以上 5 个运算符不能重载。这个经常在笔试选择题中出现。
 
下面这段代码是在类外定义运算符重载,不过要是在类外定义运算符重载的话,私有成员变量是不能在类外调用的,所以类外定义的话就得让类内的私有成员变成公有的,不过这样是不好的。
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

//private:     //如果不改为公有类外就不能调用
	int _year;
	int _month;
	int _day;
};

bool operator<(const Date& x1, const Date& x2)
{
	if (x1._year < x2._year)
	{
		return true;
	}
	else if (x1._year == x2._year && x1._month < x2._month)
	{
		return true;
	}
	else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
	{
		return true;
	}

	return false;
}

int main()
{
	Date d1(2023, 4, 26);
	Date d2(2023, 6, 21);
	cout << (d1 < d2) << endl; // 转换成operator<(d1, d2);

	return 0;
}

于是引出来了类内的运算符重载,将函数放在类内,会出现下面的问题:

这是因为 * 4.作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的 this 所以应该少一个参数。正确代码如下:
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	bool operator<(const Date& x)
	{
		if (_year < x._year)
		{
			return true;
		}
		else if (_year == x._year && _month < x._month)
		{
			return true;
		}
		else if (_year == x._year && _month == x._month && _day < x._day)
		{
			return true;
		}

		return false;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2023, 4, 26);
	Date d2(2023, 6, 21);
	cout << (d1 < d2) << endl; // 转换成d1.operator<(d2);

	return 0;
}
2.赋值运算符重载
    (1).运算符重载格式
1.参数类型 const T& ,传递引用可以提高传参效率
2.返回值类型 T& ,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
3.检测是否自己给自己赋值
4.返回 *this :要复合连续赋值的含义
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	// d4 = d1
	// d1 = d1
	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(2023, 4, 26);
	Date d2(2023, 6, 21);
	// 已经存在的两个对象之间复制拷贝        -- 运算符重载函数
	d1 = d2;

	return 0;
}

 细节上,第一点和第二点:如果不使用引用直接传值,会使用多余的拷贝构造函数,一是传进来的时候会调用,而是返回的时候调用。

   (2).赋值运算符只能重载成类的成员函数不能重载成全局函数

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

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

	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	// 前置++:返回+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;   //temp传给d,this传给d1
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	Date d1(2022, 1, 13);
	d = d1++; // d: 2022,1,13 d1:2022,1,14
	d = ++d1; // d: 2022,1,15 d1:2022,1,15
	return 0;
}

十三.const成员

const 修饰的 成员函数 称之为 const 成员函数 const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中 不能对类的任何成员进行修改。
如果Print()成员函数不加const而传入一个用const修饰的对象,相当于权限放大,这是不允许的。
权限可以缩小,但是不可以放大,所以成员函数最好加上const修饰。
只能无限制传给有限制,下面是部分情况示例,可以借鉴:

十四.初始化列表

初始化列表:以一个 冒号开始 ,接着是一个以 逗号分隔的数据成员列表 ,每个 " 成员变量 " 后面跟一个 放在括 号中的初始值或表达式。
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)
 ,_ref(ref)
 ,_n(10)
 {}
private:
 A _aobj; // 没有默认构造函数
 int& _ref; // 引用
 const int _n; // const 
};
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
* 成员变量 在类中 声明次序 就是其在初始化列表中的 初始化顺序 ,与其在初始化列表中的先后次序无关
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 随机值

上面这道题,因为类内成员声明顺序是先_a2再_a1,所以初始化的顺序也是先_a2再_a1,在初始化_a2时,_a1是未被初始化的随机值,所以_a2是随机值,_a1是1。

为了避免上面的情况(尤其是涉及到动态申请空间),我们可以像下面这样定义,比如说stack用如下方法初始化就更加合理:

class Stack
{
public:
	Stack(int capacity = 10)
		: _a((int*)malloc(capacity * sizeof(int))) //这里_capacity还没有被初始化,是随机值
		, _top(0)
		, _capacity(capacity)
	{
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			exit(-1);
		}

		// 要求数组初始化一下
		memset(_a, 0, sizeof(int) * capacity);
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main() 
{
	Stack st;

	return 0;
}

十五.隐式类型转换与explicit关键字

1.隐式类型转换
构造函数不仅可以构造与初始化对象, 对于单个参数或者除第一个参数无默认值其余均有默认值的构造函 数,还具有类型转换的作用
class A
{
public:
	A(int a)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}

	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}

private:
	int _a;
};

int main()
{
	A aa1(1);
	A aa2 = 2; // 隐式类型转换,整形转换成自定义类型
	// 2构造一个A的临时对象,临时对象再拷贝构造aa2 -->优化用2直接构造

	// error C2440: “初始化”: 无法从“int”转换为“A &”
	//A& aa3 = 2;
	const A& aa3 = 2;

	return 0;
}
const引用的特性: 1.允许绑定到临时对象;2.延长临时对象的生命周期
非const对象应该绑定到一个持久状态的对象,所以说A& aa3=2;就不能通过,因为2要构造一个A的临时对象,这个临时对象不能绑定到A&类型的对象上。
2.explicit关键字

如果不希望有隐式类型转换的出现,可以用explicit修饰构造函数,这将会禁止构造函数的隐式转换。

十六.static成员

1.概念
声明为 static 的类成员 称为 类的静态成员 ,用 static 修饰的 成员变量 ,称之为 静态成员变量 ;用 static 修饰 成员函数 ,称之为 静态成员函数 静态成员变量一定要在类外进行初始化。
class A
{
public:
	A() { ++_scount; }
	A(const A& t) { ++_scount; }
	~A() { --_scount; }
	static int GetACount()   
    { 
        return _scount; 
    }
private:
	static int _scount;  //_scount成员变量在静态区,不在类内,所以他的改变相当于是有记忆的
};

int A::_scount = 0;

void TestA()
{
	cout << A::GetACount() << endl;  //0
	A a1, a2;                        //+1  +1
	A a3(a1);                        //+1
	cout << A::GetACount() << endl;  //3
}

int main()
{
	TestA();

	return 0;
}
2.特性
1. 静态成员 所有类对象所共享 ,不属于某个具体的对象,存放在静态区
2. 静态成员变量 必须在 类外定义 ,定义时不添加 static 关键字,类中只是声明
3. 类静态成员即可用 类名 :: 静态成员 或者 对象 . 静态成员 来访问
4. 静态成员函数 没有 隐藏的 this 指针 ,不能访问任何非静态成员
5. 静态成员也是类的成员, publicprotectedprivate 访问限定符的限制
这道题就可以使用静态成员来实现,因为静态成员可以记录上之前的操作。
class Sum {
public:
    Sum()
    {
        _ret += _i;
        ++_i;
    }
    static int GetSum()
    {
        return _ret;
    }
private:
    static int _i;  //变量和包含它的函数都要使用静态成员
    static int _ret;
};

int Sum::_ret = 0;
int Sum::_i = 1;

class Solution {
public:
    int Sum_Solution(int n) {
        Sum s[n];   //n个对象
        return Sum::GetSum();
    }
};
3.两个问题
1. 静态成员函数可以调用非静态成员函数吗?
不可以。静态成员函数没有 this 指针,因此不能直接调用非静态成员函数,因为非静态成员函数需要一个实例对象来调用。静态成员函数只能访问静态数据成员和其他静态成员函数。
访问静态数据成员:
#include <iostream>
using namespace std;

class MyClass {
public:
    static void SetValue(int val) {
        _value = val;  // 访问静态数据成员
    }

    static void PrintValue() {
        cout << "Value: " << _value << endl;  // 访问静态数据成员
    }

private:
    static int _value;  // 静态数据成员
};

// 定义静态数据成员
int MyClass::_value = 0;

int main() {
    MyClass::SetValue(10);     // 调用静态成员函数来设置静态数据成员
    MyClass::PrintValue();     // 调用静态成员函数来打印静态数据成员

    return 0;
}

访问其他静态成员函数:

#include <iostream>
using namespace std;

class MyClass {
public:
    static void FunctionA() {
        cout << "FunctionA is called" << endl;
        FunctionB();  // 调用另一静态成员函数
    }

    static void FunctionB() {
        cout << "FunctionB is called" << endl;
    }
};

int main() {
    MyClass::FunctionA();  // 调用静态成员函数FunctionA,它内部调用FunctionB
    return 0;
}
2. 非静态成员函数可以调用类的静态成员函数吗?
可以。非静态成员函数可以调用静态成员函数,因为静态成员函数不依赖于特定对象的实例,可以被类的任何对象(或直接通过类名)调用。非静态成员函数可以通过 类名::静态成员函数 或直接调用静态成员函数。(如上面累加的代码)。

十七.友元函数

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装(友元可以访问私有成员),所以友元不宜多用。
友元分为: 友元函数 友元类
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;
}
(1).友元函数 可访问类的私有和保护成员,但 不是类的成员函数。
(2).友元函数 不能用 const 修饰。
(3).友元函数 可以在类定义的任何地方声明, 不受类访问限定符限制。
(4).一个函数可以是多个类的友元函数。
(5).友元函数的调用与普通函数的调用原理相同。
2.友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
(1)友元关系是单向的,不具有交换性。
比如 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问 Time
类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行。
(2)友元关系不能传递
如果 B A 的友元, C B 的友元,则不能说明 C A 的友元。
(3)友元关系不能继承
#include <iostream>
using namespace std;

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

十八.内部类

1.概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类 。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
内部类就是外部类的友元类 ,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
2.特性
(1). 内部类可以定义在外部类的 public protected private 都是可以的。
(2). 注意内部类可以直接访问外部类中的 static 成员,不需要外部类的对象 / 类名。
(3). sizeof( 外部类 )= 外部类,和内部类没有任何关系。
class A
{
private:
    static int k;
    int h;
public:
    class B  // 内部类是外部类的天生友元
    {
    public:
        void foo(const A& a)
        {
            cout << k << endl;//OK
            cout << a.h << endl;//OK
        }
    private:
        int b;
    };
};

int A::k = 1;

int main()
{
    A::B b;
    b.foo(A());   //这里构造了一个A类的匿名对象

    return 0;
}

对上面那道牛客题,我们可以进行改进,使用内部类:

class Solution {
  public:
    class Sum {
      public:
        Sum() {
            _ret += _i;
            ++_i;
        }
    };
    int Sum_Solution(int n) {
        Sum s[n];

        return _ret;
    }
  private:
    static int _i;
    static int _ret;
};

int Solution::_ret = 0;
int Solution::_i = 1;

十九.匿名对象

⽤类型(实参) 定义出来的对象叫做匿名对象,相⽐之前我们定义的 类型 对象名(实参) 定义出来的
叫有名对象。
匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
错误写法:对象和函数名冲突,不能清晰分辨
Solution sl();
sl.Sum_Solution(10);

有名对象
Solution sl;
sl.Sum_Solution(10);

匿名对象(创建一个临时性的对象)
Solution().Sum_Solution(20);

匿名对象:1.生命周期在当行;2.匿名对象具有常性;3.const引用可以延长其生命周期

class A
{
public:
    A(int a = 0)
        :_a(a)
    {
        cout << "A(int a)" << endl;
    }

    ~A()
    {
        cout << "~A()" << endl;
        _a = 0;
    }
private:
    int _a;
};

class Solution {
public:
    int Sum_Solution(int n) {
        cout << "Sum_Solution" << endl;
        //...
        return n;
    }
};

int main()
{
    A aa(1);  // 有名对象 -- 生命周期在当前函数局部域  --  形参
    A(2);     // 匿名对象 -- 生命周期在当前行  --  实参

    //A& ra = A(1);  // 匿名对象具有常性
    const A& ra = A(1); // const引用延长匿名对象的生命周期,生命周期在当前函数局部域

    Solution().Sum_Solution(20);

    return 0;
}

上面代码输出的结果:

如图可以知道,aa(1)这个对象正常调用构造函数,输出A(int a),A(2)生成一个匿名对象,输出A(int a),但是 匿名对象生命周期在当前行,所以该行结束执行析构函数~A(),下面是一个 类引用的方法,可以看到,因为 匿名对象具有常性,所以需要使用const引用,不过作为匿名对象没有立刻执行析构,先把下面的Sum_Solution执行,然后再执行ra的析构(先析构ra再析构aa),可以看到 const引用可以延长其生命周期,生命周期在当前函数局部域

二十.对象拷贝编译器优化问题(了解)

现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传参
过程中可以省略的拷⻉。
如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编
译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译还会进⾏跨
⾏跨表达式的合并优化。
#include<iostream>
using namespace std;
class A
{
public:
	A(int a = 0)
		:_a1(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
};
void f1(A aa)
{}
A f2()
{
	A aa;
	return aa;
}
int main()
{
	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;
	// 隐式类型,连续构造+拷⻉构造->优化为直接构造
	f1(1);
	// ⼀个表达式中,连续构造+拷⻉构造->优化为⼀个构造
	f1(A(2));
	cout << endl;
	cout << "***********************************************" << endl;
	// 传值返回
	// 返回时⼀个表达式中,连续拷⻉构造+拷⻉构造->优化⼀个拷⻉构造 (vs2019)
	// ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,直接变为构造。(vs2022)
	f2();
	cout << endl;
	// 返回时⼀个表达式中,连续拷⻉构造+拷⻉构造->优化⼀个拷⻉构造 (vs2019)
	// ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,直接变为构造。(vs2022)
	A aa2 = f2();
	cout << endl;

	// ⼀个表达式中,连续拷⻉构造+赋值重载->⽆法优化
	aa1 = f2();
	cout << endl;

	return 0;
}
结果在下面,可以自行在编译器上执行比对:
好了,类和对象的全部内容就在这里了,还有一个日期类的程序在下一篇有讲,可以更加深入的了解类和对象。
  • 11
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值