类与对象和模板
类的定义
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
成员函数
1.构造函数(初始化工作)
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次,不开空间创建对象,而是初始化对象。可以重载
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;
d1.Init(2022, 7, 5);
d1.Print();
Date d2;
d2.Init(2022, 7, 6);
d2.Print();
return 0;
}
构造函数时特殊的成员函数其特征如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。【意思就是既没有有参也没有无参的时候,编译器会搞一个无参的构造函数;没有无参构造但是有有参构造那么就必须要传参数调用有参构造否则报错】
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;
};
- C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类
型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看
下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员
函数。
class Time
{
private:
int _hour=1;
int _minute=1;
int _second=1;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
上述代码运行后Date d的参数为
我们注意Time类的构造函数,看一下它的运行结果,你就能明白这个差异了
class Time
{
public:
Time()
{
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour=1;
int _minute=1;
int _second=1;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
以上代码运行的结果为
我们再看一段代码,这次是Date类的区别
class Time
{
public:
Time()
{
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;//这里的1970是缺省值
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
以上代码运行结果为
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:1.无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。
2.如果是缺省构造,赋值不能跳过而是按照顺序来的
//可以调试一下下面的代码
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;
}*/
void Print() {
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year=2000;
int _month=10;
int _day=13;
};
int main() {
Date d1;
d1.Print();
//Date d2(2,3);
//d2.Print();
return 0;
}
2.析构函数(清理工作)
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。析构函数的作用是在对象生命周期结束时自动释放资源,避免内存泄漏。在对象被销毁时,编译器会自动调用析构函数。
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
函数不能重载 - 对象生命周期结束时,C++编译系统系统自动调用析构函数。
- 编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
3.拷贝构造(使用同类对象初始化创建对象)
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用。 - 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
class Date
{
public:
Date(int year, int minute, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)//d的地址08
{
Date temp(d);//temp的地址DC
return temp;//返回了一个匿名对象
}
int main()
{
Date d1(2022,1,13);//d1的地址40
Test(d1);//Test的地址2C 匿名对象
return 0;
}
以上代码运行结果为
先销毁Test函数中的temp 然后是销毁Test函数参数d 再然后是销毁Test函数返回时创建的临时对象 最后是销毁main函数中的d1
问题:什么情况下需要实现拷贝构造?
答:自己实现了析构函数释放空间,就需要实现拷贝构造
问题:拷贝构造函数中为什么不用引用传参就会引发无穷递归调用
拷贝构造函数是用来创建一个新对象并将其初始化为另一个同类型对象的副本。如果在拷贝构造函数中使用引用传参,那么它将会递归调用自身,因为传递的参数是一个对象的引用,而在函数内部又要创建一个同类型对象,这样就会无限递归调用,导致栈溢出等错误。因此,在拷贝构造函数中应该使用传值方式进行参数传递。
class A {
public:
A(const A& other) {
// 拷贝构造函数
}
};
这是一个简单的拷贝构造函数,它接受一个 A
类型的对象作为参数,并通过拷贝构造的方式创建一个新的对象。
现在,我们来看一下如果在拷贝构造函数中使用引用传参会发生什么:
class A {
public:
A(const A& other) {
A& ref = other; // 使用引用传参
}
};
在这个版本的拷贝构造函数中,我们使用了引用传参,将 other
对象的引用赋值给了 ref
变量。这样做是没有问题的,因为引用传参只是将原对象的地址传递给了新对象,不会创建新的对象。
但是,如果我们使用了值传参,会发生什么呢?
class A {
public:
A(A other) { // 使用值传参
// ...
}
};
在这个版本的拷贝构造函数中,我们使用了值传参,将 other
对象作为值传递给了新对象。这意味着会创建一个新的 A
对象,并将 other
对象的值复制到新对象中。
但是,这个过程中会调用拷贝构造函数,因为我们要将 other
对象的值复制到新对象中,而这个过程就需要调用拷贝构造函数。因此,如果我们使用值传参,就会陷入无穷递归调用的死循环中,直到栈溢出或程序崩溃为止。
简单的拷贝构造函数
以下是一个简单的拷贝构造函数的示例代码,但是是浅拷贝,其中有注释解释每一行代码的作用:
#include <iostream>
class MyClass {
public:
// 默认构造函数
MyClass() {
std::cout << "Default constructor called." << std::endl;
}
// 拷贝构造函数
MyClass(const MyClass& other) {
std::cout << "Copy constructor called." << std::endl;
// 将other对象的成员变量值拷贝到当前对象中
this->m_value = other.m_value;
}
// 成员变量
int m_value = 0;
};
int main() {
// 创建一个MyClass对象
MyClass obj1;
obj1.m_value = 10;
// 使用拷贝构造函数创建一个新的MyClass对象,并将其赋值给obj2
MyClass obj2 = obj1;
std::cout << "obj2.m_value = " << obj2.m_value << std::endl;
return 0;
}
4.赋值运算符重载
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year;
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
浅拷贝是指将对象的指针成员变量直接指向原对象的指针成员变量所指向的内存空间,这样会导致两个对象共享同一块内存空间,当其中一个对象改变了内存空间中的值,另一个对象也会受到影响。这种拷贝方式适用于对象中没有指针成员变量的情况。
深拷贝是指在拷贝对象时,不仅要拷贝对象本身的数据成员,还要拷贝动态分配的内存,使得目标对象和源对象的指针指向不同的内存空间。
如果没有用深拷贝会出现的问题:
- 原来的空间丢失了,存在内存泄露
- 两者共享同一份空间,最后销毁时会导致同一份内存空间释放两次从而引起程序崩溃
深拷贝和浅拷贝的区别
深拷贝(deep copy)和浅拷贝(shallow copy)是指在对象拷贝过程中对于指针类型成员的处理方式不同,导致拷贝出来的对象的指针成员所指向的内存地址不同。
深拷贝的例子代码:
#include <iostream>
#include <cstring>
using namespace std;
class DeepCopy {
public:
DeepCopy(const char* str) {
m_str = new char[strlen(str) + 1];
strcpy(m_str, str);
}
DeepCopy(const DeepCopy& other) {
m_str = new char[strlen(other.m_str) + 1];
strcpy(m_str, other.m_str);
}
~DeepCopy() {
delete[] m_str;
}
void Print() {
cout << "m_str = " << m_str << endl;
}
private:
char* m_str;
};
int main() {
DeepCopy str1("Hello World");
DeepCopy str2 = str1; // 深拷贝
str1.Print(); // 输出 "m_str = Hello World"
str2.Print(); // 输出 "m_str = Hello World"
str1.~DeepCopy(); // 释放 str1 的内存
str2.Print(); // 输出 "m_str = Hello World",因为 str2 拷贝时重新分配了内存
return 0;
}
浅拷贝的示范代码:
#include <iostream>
#include <cstring>
using namespace std;
class ShallowCopy {
public:
ShallowCopy(const char* str) {
m_str = new char[strlen(str) + 1];
strcpy(m_str, str);
}
ShallowCopy(const ShallowCopy& other) {
m_str = other.m_str;
}
void Print() {
cout << "m_str = " << m_str << endl;
}
private:
char* m_str;
};
int main() {
ShallowCopy str1("Hello World");
ShallowCopy str2 = str1; // 浅拷贝
str1.Print(); // 输出 "m_str = Hello World"
str2.Print(); // 输出 "m_str = Hello World"
str1.~ShallowCopy(); // 释放 str1 的内存
str2.Print(); // 输出 "m_str = ",因为 m_str 已被释放
return 0;
}
友元
友元函数
友元函数可以修改类的私有成员。因为友元函数被声明为类的友元,所以它可以访问类的私有成员,包括修改它们的值。
class MyClass {
private:
int myPrivateVar;
public:
MyClass(int x) : myPrivateVar(x) {}
friend void modifyMyPrivateVar(MyClass& obj, int newVal) {
obj.myPrivateVar = newVal;
}
};
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。
比如上述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;
};
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值
的构造函数,还具有类型转换的作用。用explicit修饰构造函数,将会禁止构造函数的隐式转换。
#include <iostream>
using namespace std;
class MyClass {
public:
explicit MyClass(int value) : m_value(value) {
cout << "MyClass(int) constructor called with value: " << m_value << endl;
}
private:
int m_value;
};
// 该函数的参数使用了explicit关键字,只能用于显式调用构造函数
void printMyClass(const MyClass& obj) {
cout << "MyClass object with value: " << obj << endl;
}
int main() {
// 编译错误:不能隐式调用MyClass构造函数
// MyClass obj1 = 10;
// 正确:显式调用MyClass构造函数
MyClass obj2(10);
// 编译错误:不能隐式调用MyClass构造函数
// printMyClass(10);
// 正确:显式调用MyClass构造函数
printMyClass(MyClass(10));
return 0;
}
Static关键字
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用
static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,
它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越
的访问权限。
注意:内部类天生就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访
问外部类中的所有成员。但是外部类不是内部类的友元。单向关系
特性:
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- 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;
}
匿名对象
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;
}
模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的Add版本
}
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
// 专门处理int的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换