C++初阶2:类和对象 - 详解

目录

一、类的定义

二、实例化

三、this指针

四、类的默认成员函数

五、构造函数

六、析构函数

七、拷贝构造函数

八、赋值运算符重载

九、取地址运算符重载

十、初始化列表

十一、类型转换

十二、static成员

十三、友元

十四、内部类

十五、匿名对象

十六、对象拷贝时的编译器优化


一、类的定义

class为定义类的关键字,使用时的格式就像下面一样,Month为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
在C++struct也可以兼容,class和struct功能有些类似但使用方法上大有不同。
class Month
{
	int a;
	char c;
    public:
	void Printf()
	{
		cout << a << endl;
	}
};
int main()
{
	Month a;
	a.Printf();
	return 0;
}

其中我们用到的public是访问限定符,除了public之外还有为private和protected,这是C++一种类似访问权限的方法,通过访问权限选择性的将其接口提供给外部的用户使用。

在现阶段我们只能知道的,public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,protected和private是⼀样的,以后学习才能体现出他们的区别。

访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有
访问限定符,作用域就到 }即类结束。class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	// 为了区分成员变量,一般习惯上成员变量前加上 _
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	d.Init(2024, 3, 31);
	return 0;
}
类定义了⼀个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用:: 作
用域操作符指明成员属于哪个类域。这是C++的查找规则有关,如果不用::表明我们定义的函数是那个域,那这个函数就成了全局函数,不能使用那个域里面protected和private下的变量。如下
class Date
{
public:
	void Init(int year, int month, int day);
private:
	// 为了区分成员变量,一般习惯上成员变量前加上 _
	int _year;
	int _month;
	int _day;
};
//没显示函数是Data域
void Init(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}
//有显示
void Date::Init(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}
int main()
{
	Date d;
	d.Init(2024, 3, 31);
	return 0;
}

二、实例化

用类类型在物理内存中创建对象的过程,称为类实例化出对象。类是对象的一种抽象化的描述,只是说明了有那些实例对象但这些对象还没有被创建,他就像是房子的设计图纸,房子还没有被创建出来。实例化出的对象才可以分配物理内存存储数据。
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
	Date d1;
	Date d2;
	d1.Init(2025, 11, 18);
	d1.Print();
	d2.Init(2025, 11, 19);
	d2.Print();
	return 0;
}
类实例化出的每个对象,都有独立的数据空间,但函数被编译后是一段指令,对象中没办法存储,这些指令存储在⼀个单独的区域(代码段),如果需要储存也只能储存它的指针,而且它并不是每次实例化成d1或d2都被存储一次,我们想一下,如果每次实例化都储存一次,那我们实例化1000次就需要储存1000次,这样很浪费空间。其实函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址。
C++规定类实例化的对象也要符合内存对齐的规则对齐的规则我们在C语言中说过,所以这里简单提一下。

1. 基本数据类型的对齐值,每个基本类型有默认的自身对齐值(通常等于其字节大小)

2. 结构体的内存对齐规则,结构体的对齐需满足3条核心规则:

    1.    成员对齐:结构体每个成员的起始地址,必须是其“自身对齐值”的整数倍;

    2.    整体对齐:结构体的总大小,必须是其“最大成员对齐值”的整数倍;

    3.    编译器对齐单位:若编译器指定了对齐单位(如#pragma pack(n)),则对齐值取“自身对齐值”与n的较小值。

如果实例化对象没有成员变量也会占一个字节,因为如果一个字节都不给,怎么表示对象存在过呢!

class A
{
public:
	void Print()
	{
		cout << _ch << endl;
	}
private:
	char _ch;
	int _i;
};
class B
{
public:
	void Print()
	{
		//...
	}
};
class C
{};
int main()
{
	A a;
	B b;
	C c;
	cout << sizeof(a) << endl;
	cout << sizeof(b) << endl;
	cout << sizeof(c) << endl;
	return 0;
}

三、this指针

我们在一个类中调用函数时,函数体中没有关于不同对象的区分,那该如何知道函数访问的是那个对象呢。C++有一个隐含的this指针解决这里的问题。编译器编译后,类的成员函数默认都会在形参第一个位置,增加一个当前类类型的指针,叫做this指针。所以我们看下面的例子,我们写这个函数是这样的,但本质上它是那样的。

但是不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。

// void Init(Date* const this, int year, int month, int day)
	/*void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/
	//可以这样写
	void Init(int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}

四、类的默认成员函数

类是默认成员函数是指我们没有主动显示实现但编译器会自己主动生成的成员函数。一个类,我 们不写的情况下编译器会默认生成成以下6个默认成员函数,这里我们主要了解前四个。有人可能会疑惑为什么我们要主动了解这些函数,这难道不是编译器会帮我们解决的吗?我们是要搞明白编译器生成了什么样的成员函数帮我们解决了问题,如果有编译器生成的函数无法解决的问题我们又该如何自己去实现。

五、构造函数

构造函数是特别的成员函数,但我们要注意构造函数的本质并不是去开辟空间。虽然它可以做到,但它的本质是一个类似于初始化的过程。就像下面我们的类Data中实例化出对象但没有开辟空间。但是对于Stack它就需要开辟空间。所以说我们可以把它理解为以前写Satck时Init的作用。

#include
using namespace std;
class Data
{
public:
	//无参
	Data( )
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	//带参
	Data (int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//全缺省
	Data(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print( )
	{
		cout << _year << '/' << _month << '/' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Data d1;
	Data d2(2025, 11, 19);
	Data d3();
	d1.Print();
	d2.Print();
	d3.Print();
}
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
构造函数的特点是函数名与类名相同。无返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此) 。对象实例化时系统会自动调用对应的构造函数。构造函数可以重载。如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造。
class Data
{
public:
	//无参
	Data( )
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	//带参
	Data (int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//全缺省
	Data(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print( )
	{
		cout << _year << '/' << _month << '/' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

我们要注意如果使用无参构造函数时不要在对象后面跟括号,否则编译器不知道你是想实例化对象还是函数声明。

int main()
{
	Data d1;
	Data d2(2025, 11, 19);
	Data d3();
	d1.Print();
	d2.Print();
	//d3.Print();
还有编译器默认生成的构造,对内置类型(int ,double,char)成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型(class,struct)成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决。

编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了初始化

六、析构函数

析构函数与构造函数功能相反,析构函数也不是销毁的意思,因为局部对象是存在栈帧的,函数结束的时候就自己释放了所以不用我们操心。析构函数更像是避免资源浪费,像我们在Stack里的的Destroy功能,但是我们之前写的Data就不需要析构函数,因为它没有资源需要释放,就差不多是没有申请过空间,不需要把指针置空等等。

析构函数的特点:

1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。 (这里跟构造类似,也不需要加void)
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,系统会自动调用析构函数。
5. 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
6. 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数
7.有资源申请时一定要自己写析构函数,编译器 自动生成默认的析构函数对资源的释放不彻底。
果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue(因为它是底层还是Stack,所以我们写了Stack的析构函数,MyQueue的析构函数就不写默认的析构函数也是会调用Stack);但是有资源申请时,一定要自己写析构,否则会造成资源泄漏,如Stack。

注意:一个局部域的多个对象,C++规定后定义的先析构。

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
class MyQueue
{
public:
private:
	Stack pushst;
	Stack popst;
};
int main()
{
	Stack st;
	MyQueue mq;
	return 0;
}

七、拷贝构造函数

拷贝函数是一个特殊的构造函数。因为C++规定在传值传参时要调用拷贝构造,所以我们实现拷贝构造时要传引用传值。如果直接传值传参会形成新的拷贝构造会无限递归下去。如下

class Data
{
public:
	void Print()
	{
		cout << _year << '/' << _month << '/' << _day << endl;
	}
	Data(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Data(Data& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Data d1(2025, 11, 20);
	Data d2(d1);
	d2.Print();
}
拷贝构造函数的第一个参数必须是类类型对象的引用,可以有多个参数但后面的参数必须有缺省值。C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。而编译器自己默认生成的拷贝构造函数只是一种“浅拷贝/值拷贝”并不适用很多情况,比如我们用一个值拷贝拷贝Stack类型的对象,因为它其中一个成员变量是指针,如果值拷贝会导致两个对象的地址是指向同一个的,后续使用的过程中无论是添加数据还是解析等都会有很大问题。
MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现MyQueue的拷贝构造
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	Stack(Stack& st)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}
	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
				sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(2);
	//和Stack st2(st1)一样
	Stack st2 = st1;
	//Data那个也可以写成Data d2 = d1
	return 0;
}

补充

有人可能对这几种传值的方式不太理解,这里给大家总结一下

    •    传值返回:返回变量的副本,函数结束后原局部变量销毁,副本独立存在,开销=变量大小(适合基础类型)。

    •    指针返回:返回变量的内存地址,需确保指向的变量(如全局/静态变量、堆内存)生命周期长于函数,避免返回局部变量指针(悬垂指针)。

    •    引用返回:返回变量的别名(无副本开销),本质是指针优化,必须指向有效变量(不能返回局部变量引用),可直接修改原变量。

// 传值返回
int add(int a, int b) {
    int res = a + b;
    return res; // 返回res的副本
}
// 指针返回(静态变量,生命周期长)
int* getStaticNum() {
    static int num = 10;
    return # // 安全,静态变量在程序结束前不销毁
}
// 引用返回(返回全局变量别名)
int g_num = 20;
int& getGlobalNum() {
    return g_num; // 安全,全局变量生命周期贯穿程序
}

八、赋值运算符重载

C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。
函数在类外面:重载运算符函数的参数个数和该运算符作用的运算对象数量⼀样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。(一元就类似于++,--,+=,-=。二元就类似于-,+,==,<,>)
我们要注意当重载运算符函数在类外面时在C++中是不允许重载内置类型(int,double)的运算符的
函数在类里面:如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少⼀个。运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
#include
using namespace std;
class Data
{
public:
	void Print()
	{
		cout << _year << '/' << _month << '/' << _day << endl;
	}
	Data(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Data operator+(const Data& d2)
	{
		return Data(_year + d2._year,
			_month + d2._month,
			_day + d2._day);
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Data d1(2025,11,22);
	d1.Print();
	Data d2(0, 0, 2);
	Data d3 = d1 + d2;
	d3.Print();
	Data d4 = d1.operator+(d3);
	d4.Print();
	return 0;
}
不能通过连接语法中没有的符号来创建新的操作符:比如operator@,而且有五个符号不能重载(sizeof     .*     ::     .     ?:)。
重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。
C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。
#include
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	Date& operator++()
	{
		cout << "前置" << endl;
		_day++;
		return *this;
	}
	Date& operator++(int)
	{
		cout << "后置" << endl;
		Date tmp = *this;
		_day++;
		return tmp;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2025,11,22);
	d1.Print();
	Date d2 = d1++;
	d2.Print();
	d1.Print();
	Date d3 = ++d1;
	d3.Print();
	d1.Print();
	return 0;
}
重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第一个形参位
置,第一个形参位置是左侧运算对象,调用时就变成了 对象<<cout,不符合使用习惯和可读性。
重载为全局函数把ostream/istream放到第一个形参位置就可以了,第二个形参位置当类类型对
象。而且如果我们重载为全局函数就面临无法访问类中被限定为私有的成员变量,我们的解决办法是友元函数(后面会详细说明)/成员放为公有/重载为成员函数/
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷拷贝赋值,它和拷贝构造的区别是拷贝构造把a拷贝给b,b是正在被创建;赋值是把a赋值给b,b是已经被创建好的。
我们来总结一下几种构造,看一下各自的使用情况
#include
using namespace std;
class Date
{
public:
	//构造
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//赋值
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this;
	}
	// 拷贝构造
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	// 普通自定义构造
	Date(const Date* d)
	{
		_year = d->_year;
		_month = d->_month;
		_day = d->_day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	//构造
	Date d1(2025, 11, 23);
	d1.Print();
	// 拷贝构造
	Date d2(d1);
	d2.Print();
	// 普通自定义构造
	Date d3(&d1);
	d3.Print();
	//赋值
	Date d4 = d1;
	d4.Print();
	return 0;
}

我们注意到对于赋值运算符重载的返回值我们一般都是引用返回,这样做也是为了避免发生拷贝,也为了支持连续赋值场景。

而且赋值运算符重载也和前面提到过的Stack的情况一样,像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载,也不需要我们显示实现MyQueue的赋值运算符重载。

我们学习到这个地方后就可以解决日期类的计算问题,大家可以自己尝试一下,我的源代码链接在这里,关于这里还有一些细节,下面我也会为大家解答。https://gitee.com/chen-chen-chen-chen-ch/c-.cpp/commit/44ec2dd5a5697f6b7c34e8192f72f1cd0ffacd49

九、取地址运算符重载

如果大家看到了我的日期类计算代码就会看到我在很多成员函数的后面都加了const,这里的const其实是在修饰成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。比如void Print()函数的隐含的this指针就由Date* const this 变成了const Date* const this.

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般编译器自己生成的就够我们使用了,不用特意去构造。除非有时候我们有需要不想让别人取到当前类的地址就可以自己实现一个,随便返回一个地址。
class Date
{
public:
	 Date * operator&()
	{
		return this;
	    return nullptr;
	}
	const Date * operator&()const
	{
		return this;
		// return nullptr;
	}
private:
	 int _year;
	 int _month;
	 int _day;
};

十、初始化列表

初始化列表实际上也是一种构造函数的方法,我们使用构造函数初始化成员变量是在函数体内进行赋值,初始化列表是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。(初始化列表就是在构造函数的函数体外)无论是否显示写初始化列表,每个构造函数都有初始化列表。每个成员变量只能使用一次。

尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这 个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没 有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有 显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构 造会编译错误。

还有三个特殊的变量必须放在初始化列表位置进行初始化,否则会编译报错。

#include
using namespace std;
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};
class Date
{
public:
	Date(int& x, int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
		, _t(12)
		, _ref(x)
		, _n(1)
	{
	}
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
	int& _ref;
	const int _n;
};
int main()
{
	int i = 0;
	Date d1(i);
	d1.Print();
	return 0;
}

C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的 成员使用的。(注意这里的缺省值和我们在构造函数中说的那个不能同等理解,这里的缺省值是只有初始化列表没有初始化的时候才会使用)

#include
using namespace std;
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};
class Date
{
public:
	Date()
		:_month(2)
	{
		cout << "Date()" << endl;
	}
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day;
	Time _t = 1;
	const int _n = 1;
	int* _ptr = (int*)malloc(12);
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

一般按声明顺序初始化。

十一、类型转换

C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。换句话说,C++允许把内置类型(int,double这种基础数据类型)自动转化成某个类的对象但前提是这个类要有相应的构造函数来支持这种转换。然后构造函数前面加explicit就不再支持隐式类型转换。
#include 
using namespace std;
//转换
class A
{
public:
	A(int a)
	{
		_a = a;
	}
	void Print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A aa = 1;
	aa.Print();
	return 0;
}
//不能转换
class A
{
public:
	explicit A(int a)
	{
		_a = a;
	}
	void Print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A aa = 1;
	aa.Print();
	return 0;
}

十二、static成员

用static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化。所有类对象所共享,存放在静态区。
用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针,静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。但非静态成员函数可以访问静态成员和非静态成员。
静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员
变量不属于某个对象,不走构造函数初始化列表。
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;
int main()
{
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);
	cout << A::GetACount() << endl;
	cout << a1.GetACount() << endl;
	return 0;
}

十三、友元

友元提供了⼀种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类
声明的前面加friend,并且把友元声明放到一个类的里面。代表这个函数或者类可以使用我们这个类里面的成员函数和成员变量。
友元函数仅仅是一种声明,他不是类的成员函数,可以在类定义的任何地方声明,不受类访问限定符限制。
#include
using namespace std;
class B;
class A
{
	// 友元声明
	friend void func(const A& aa, const B& bb);
private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
	// 友元声明
	friend void func(const A& aa, const B& bb);
private:
	int _b1 = 3;
	int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
	cout << aa._a1 << endl;
	cout << bb._b1 << endl;
}
int main()
{
	A aa;
	B bb;
	func(aa, bb);
	return 0;
}

十四、内部类

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在
全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。我们可以把内部类当作外部类的友元类。
#include
using namespace std;
class A
{
private:
	static int _k;
	int _h = 1;
public:
	class B
	{
	public:
		void foo(const A& a)
		{
			cout << _k << endl; //OK
			cout << a._h << endl; //OK
		}
		int _b1;
	};
};
int A::_k = 1;
int main()
{
	cout << sizeof(A) << endl;
	A::B b;
	A aa;
	b.foo(aa);
	return 0;
}

十五、匿名对象

用类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参) 定义出来的叫有名对象,它的生命周期只在当前一行,一般临时定义一个对象当前用⼀下即可,就可以定义匿名对象。
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();
	A(1);
	A aa2(2);
	Solution().Sum_Solution(10);
	return 0;
}

十六、对象拷贝时的编译器优化

编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返
回值的过程中可以省略的拷贝。这里不再过多赘述,我们只要知道在匿名对象传参,隐式类型转换传参,具有局部对象返回,匿名对象返回,对象之间初始化等场景下可以明显看出优化效果。
class A
{
public:
	//构造
	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		, _a2(a2)
	{
		cout << "A(int a1 = 0, int a2 = 0)" << 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()
	{
		//delete _ptr;
		cout << "~A()" << endl;
	}
	void Print()
	{
		cout << "A::Print->" << _a1 << endl;
	}
	A& operator++()
	{
		_a1 += 100;
		return *this;
	}
private:
	int _a1 = 1;
	int _a2 = 1;
};
//int main()
//{
//	A aa1 = 1;
//	const A& aa2 = 1;
//
//	return 0;
//}

posted on 2025-12-22 19:40  ljbguanli  阅读(0)  评论(0)    收藏  举报