C++——类与对象(二)
qing_040603 2024-09-12 14:05:02 阅读 92
目录
引言
类的默认成员函数
构造函数
1.构造函数的概念
2.注意事项
初始化列表
1.初始化列表的概念
2.注意事项
析构函数
1.析构函数的概念
2.注意事项
拷贝构造函数
1.拷贝构造函数的概念
2.注意事项
运算符重载
1.运算符重载的概念
2.注意事项
赋值运算符重载
1.赋值运算符重载的定义
2.注意事项
取地址运算符重载
1.const成员函数
2.取地址运算符重载
结束语
引言
在C++——类与对象(一) 我们学习类与对象的一些基础知识,接下来我们接着学习。
类的默认成员函数
在C++中,当你定义一个类时,即使没有显式地声明某些成员函数,编译器也会为该类自动生成一些默认的成员函数。
⼀个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最 后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数, 移动构造和移动赋值,这个我们后面再讲解。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:
第一:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求
第二:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?
构造函数
1.构造函数的概念
构造函数是一种特殊的方法,主要用于在创建对象时初始化对象。在面向对象编程(OOP)中,构造函数是类的一个组成部分,它当创建类的新实例时自动调用。构造函数的主要目的是为新创建的对象分配初始值或执行必要的启动操作。
其特点如下:
1.函数名与类名相同。
2. 无返回值。(返回值都不需要给,也不需要写void,不要纠结,C++规定如此)
3. 对象实例化时系统会自动调用对应的构造函数。
4. 构造函数可以重载。
下面是一个日期类的构造函数:
<code>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 d1(2024,9,1);//自动调用
d1.Print();
return 0;
}
输出结果如下:
构造函数的功能就相当于初始化函数。构造函数通过其自动调用的特性,在提升代码的容错率和可维护性方面发挥了重要作用。它们确保了每个对象在创建时都能以预期的方式被初始化,减少了因未初始化或错误初始化而导致的错误。
2.注意事项
(1)如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
来看个简单的例子:
<code>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;
};
(2)无参构造函数、全缺省构造函数以及编译器默认生成的构造函数,统称为默认构造函数,因为它们都允许在不传递实参的情况下进行对象初始化。在同一个类中,无参构造函数和全缺省构造函数不能同时存在,因为它们之间构成函数重载但调用时会引发歧义。简而言之,默认构造指的是无需显式提供实参即可调用的构造函数。
比如如下代码:
class Date
{
public:
Date()//无参
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 2024, int month = 9, 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()
{
Date d;// 引起混淆
return 0;
}
如下所示:
在C++中,如果一个类定义了多个构造函数,一旦我们对对象进行实例化,那么编译器在尝试调用这些构造函数时就会遇到歧义,因为它不知道应该选择哪一个。
(3)编译器默认生成的构造函数对内置类型成员变量不进行显式初始化(初始化不确定),但会尝试调用自定义类型成员变量的默认构造函数进行初始化。
举个简单的例子,看下面的代码:
<code>class A
{
public:
A()
{
cout << "hello world" << endl;
}
private:
int _a;
};
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
A a;
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d.Print();
return 0;
}
输出结果为:
Date 类的成员变量 _year、_month 和 _day 没有被初始化,这将导致 Print() 方法输出不确定的值。
我们可以得知:编译器自动生成的默认构造函数只对自定义类型进行初始化。
到后来,C++11 中针对内置类型成员不初始化的缺陷,进行了优化,即:内置类型成员变量在类中声明时可以给默认值。
如下所示:
<code>class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year = 1;//缺省值
int _month = 1;//缺省值
int _day = 1;//缺省值
};
int main()
{
Date d;
d.Print();
return 0;
}
输出结果如下:
初始化列表
1.初始化列表的概念
初始化列表是C++中构造函数的一个特性,它允许在构造函数体执行之前,直接初始化对象的成员变量或基类。使用初始化列表可以提高效率,特别是对于那些需要复杂构造的成员变量或基类,因为它避免了先调用默认构造函数然后再进行赋值的开销。
初始化列表位于构造函数参数列表之后,构造函数体的大括号 {} 之前,由冒号<code>:引导。
举个简单的例子:
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 d(2024, 9, 1);
d.Print();
return 0;
}
输出结果为:
2.注意事项
(1)成员初始化顺序
成员变量的初始化顺序是由它们在类中的声明顺序决定的,而不是在初始化列表中的顺序。如果尝试在初始化列表中改变这个顺序,可能会导致未定义行为。
像这样:
<code>class MyClass
{
public:
MyClass() : b(a), a(10)
{
} // 错误:a 在 b 之后初始化,尽管在初始化列表中 b 写在前面
private:
int a;
int b; // b 依赖于 a 的值,但 a 还未初始化
};
(2)每个成员变量只能初始化一次
(3)必须放在初始化列表中的成员
类中包含以下成员,必须放在初始化列表位置进行初始化:const成员变量,引用成员变量,自定义类型成员(且该类没有默认构造函数时)。因为这些变量都需要在定义时初始化。
const成员变量:const成员变量必须在构造时初始化,且之后不能修改,因此必须放在初始化列表中。
引用成员变量:引用必须在定义时初始化,并且之后不能重新指向另一个对象,因此也必须放在初始化列表中。
没有默认构造函数的自定义类型成员:如果类的成员是自定义类型,且该类型没有默认构造函数(或默认构造函数被声明为delete),则必须在初始化列表中显式地初始化该成员。
像这样:
class A
{
public:
A(int a):
_a(a)
{
// ...
}
private:
int _a;
};
class B
{
public:
B(int a, int ret)
:_b(a)
, _ret(ret)
, _n(3)
{
// ...
}
private:
A _b;// 没有默认构造函数的自定义类型成员
int& _ret;// 引用
const int _n;// const常量
};
(3)尽量使用初始化列表初始化,通常使用初始化列表,对于自定义类型成员变量,会先使用初始化列表初始化。
如下所示:
#include <iostream>
using namespace std;
class A
{
public:
A(int a) : _a(a) // 使用初始化列表初始化成员变量
{
cout << "A(int a)" << endl;
}
private:
int _a;
};
class B
{
public:
B(int a, int b)
: _a(b), _b(a) // 先初始化 _a 然后初始化 _b
{
cout << "B(int a, int b)" << endl;
}
private:
A _a; // A 的构造函数会先于 B 的构造函数执行
int _b;
};
int main()
{
B b(2, 3);
return 0;
}
输出结果:
析构函数
1.析构函数的概念
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。
其特点为:
1.析构函数名是在类名前加上字符 ~。
2.无参数无返回值。(这里跟构造类似,也不需要加void)
3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4.对象生命周期结束时,系统会自动调用析构函数。
下面是一个简单的示例,同样是日期类:
<code>class Date
{
public:
Date(int year = 2024, int month = 9, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//析构函数
~Date()
{
_year = _month = _day = 0;
}
private:
int _year;
int _month;
int _day;
};
析构函数相当于C语言中的销毁函数,确保了在对象销毁时,所有由该对象使用的资源都能得到妥善处理,从而避免资源泄露和其他潜在问题。
并且它是自动调用的,可以大大提高代码的容错率。
2.注意事项
(1)如果类中没有显式定义析构函数,则C++编译器会自动生成一个析构函数,一旦用户显式定义编译器将不再生成。
class Date
{
public:
Date(int year = 2024, int month = 9, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//析构函数
/*~Date()
{
_year = _month = _day = 0;
}*/
// 编译器会自动生成一个析构函数
private:
int _year;
int _month;
int _day;
};
(2)默认的析构函数对于内置类型(如int、float等)的成员变量不做任何处理,因为内置类型的生命周期是自动管理的,对于自定义类型调用其析构函数。
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
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;
}
//默认生成
private:
A a;
int _year;
int _month;
int _day;
};
int main()
{
Date date(2024, 9, 1);
date.Print();
// 当 date 离开作用域时,其析构函数将被调用
return 0;
}
输出结果为:
拷贝构造函数
1.拷贝构造函数的概念
拷贝构造函数是一种特殊的构造函数,只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),用于创建一个对象作为另一个同类型对象的副本。
其特点如下:
拷贝构造函数是构造函数的一个重载
举个例子:
<code>class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = 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(2024, 9, 1);
// 使用拷贝构造函数创建d1的副本d2
Date d2(d1);//拷贝构造
// 使用拷贝初始化(也是拷贝构造的一种形式)创建d1的副本d3
Date d3 = d1;//拷贝构造
d1.Print();
d2.Print();
d3.Print();
return 0;
}
输出结果为:
2.注意事项
(1)拷贝构造函数的参数只有⼀个且必须是类类型对象的引用,使⽤传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用
错误代码如下:
<code>Date(const Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
(2)如果类没有显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数。这个默认的拷贝构造函数会按照对象的内存布局逐字节地复制数据(即浅拷贝或值拷贝)。这意味着对于类中的基本数据类型成员和指针成员,它们的值(或地址)会被直接复制到新创建的对象中。
如下所示:
#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)
{
_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(2024, 9, 1);
// 使用拷贝构造函数创建d1的副本d2
Date d2(d1);//拷贝构造
// 使用拷贝初始化(也是拷贝构造的一种形式)创建d1的副本d3
Date d3 = d1;//拷贝构造
d1.Print();
d2.Print();
d3.Print();
return 0;
}
输出结果如下:
(3)编译器默认生成的拷贝构造函数执行的是值拷贝(也称为浅拷贝),它会逐字节地复制对象的数据。在大多数情况下,这对于只包含基本数据类型(如整数、浮点数等)的对象来说是足够的。但是在某些场景会出错。
来看个例子:
<code>class A
{
public:
A() :
data(new int(10))
{
} // 构造函数中动态分配内存
~A()
{
delete data;
} // 析构函数中释放内存
// 注意:这里没有显式定义拷贝构造函数
void Print() const
{
std::cout << *data << std::endl;
}
private:
int* data; // 指向动态分配内存的指针
};
int main()
{
A a1; // 创建第一个对象,分配内存
A a2 = a1;
// 拷贝构造第二个对象,但这里使用的是默认拷贝构造函数
// 现在 a1 和 a2 共享同一块内存
a1.Print(); // 输出 10
a2.Print(); // 输出 10
// 当 main 函数结束时,a2 和 a1 的析构函数将被调用
// 由于它们共享同一块内存,这块内存将被释放两次,
// 导致未定义行为
return 0;
}
如果类中有一个指向动态分配内存的指针,并且你没有在拷贝构造函数中显式地管理这块内存的复制(即没有创建新的内存副本并将指针指向它),那么原始对象和新对象将共享同一块内存。这可能导致在对象析构时内存被多次释放或悬挂指针等问题。
运算符重载
1.运算符重载的概念
运算符重载(Operator Overloading)是C++中的一个重要特性,它允许程序员为已有的运算符(如+、-、*、/等)赋予新的含义,以便它们能够用于用户自定义的类型(如类)上。这意味着我们可以定义运算符如何作用于类的对象,从而使对象的使用更加直观和方便。运算符重载是具由运算符operator定义有特殊函数名的函数。
下面是个简单的示例,判断两个日期是否相等的运算符重载:
<code>using namespace std;
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;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2024, 7, 6);
Date d2(2024, 8, 9);
if (d1 == d2)
{
cout << "相等" << endl;
}
else
{
cout << "不相等" << endl;
}
return 0;
}
2.注意事项
(1)重载运算符函数的参数个数和该运算符作用的运算对象数量⼀样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
(2)如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的 this 指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
像这样:
class Point
{
public:
int x, y;
Point(int x = 0, int y = 0) : x(x), y(y)
{
}
// 重载+运算符,接受另一个Point对象作为参数
Point operator+(const Point& rhs)
{
return Point(x + rhs.x, y + rhs.y);
}
void print() const
{
cout << "(" << x << ", " << y << ")" << endl;
}
};
int main()
{
Point p1(1, 2), p2(3, 4), p3;
p3 = p1 + p2;
p3.print();
return 0;
}
(3)运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
(4)不能通过连接语法中没有的符号来创建新的操作符:比如operator@。
(5) .(成员访问运算符)、.*(指向成员的指针访问运算符)、::(作用域解析运算符)、sizeof(长度运算符)、? :(条件运算符)等操作符不能重载。
(6)重载操作符至少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y) 。
(7)重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。 C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。
像这样:
Date Date::operator++(int) // 注意这里的int参数,它是区分前置和后置的关键
{
Date tmp = *this; // 创建一个当前对象的副本
*this += 1; // 增加原对象的值
return tmp; // 返回增加之前的对象副本
}
Date& Date::operator++() // 注意这里没有参数
{
*this += 1; // 增加原对象的值
return *this; // 返回原对象的引用
}
赋值运算符重载
1.赋值运算符重载的定义
赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象。
赋值运算符重载是将运算符 = 进行运算符重载,如下所示:
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)//赋值运算符重载
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
// 打印函数
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 9, 1);
Date d2;
d2 = d1; // 调用赋值运算符
d2.Print();
Date d3;
d3 = d2 = d1; // d2 = d1 后返回 d2 的引用,然后 d3 = d2
d3.Print();
return 0;
}
输出结果:
2.注意事项
(1)有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
(2)没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝,对自定义类型成员变量会调用他的拷贝构造。
<code>class Time
{
public:
// 默认构造函数
Time()
{
_hour = 0;
_minute = 0;
_second = 0;
}
// 构造函数,接受小时、分钟和秒
Time(int hour, int minute, int second)
{
_hour = hour;
_minute = minute;
_second = second;
}
// 赋值运算符重载
Time& operator=(const Time& t)
{
cout << "Time& operator=(const Time& t)" << endl;
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
// 成员函数用于打印时间
void Print() const
{
cout << _hour << ":" << _minute << ":" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year = 2024;
int _month = 9;
int _day = 1;
Time _t;
};
int main()
{
Time t1; // 调用默认构造函数
t1.Print(); // 输出: 0:0:0
Time t2(12, 15, 30); // 使用新定义的构造函数
// 显式调用赋值运算符重载
t1 = t2; // 将t2的值赋给t1
t1.Print();
return 0;
}
输出结果为:
(3)因为编译器默认生成默认赋值运算符重载的是值拷贝,在某些场景下就会出错。与拷贝构造函数类似。
<code>class A
{
public:
A()
{
cout << "A created\n"; // 当A的实例被创建时打印
}
~A()
{
cout << "A destroyed\n"; // 当A的实例被销毁时打印
}
};
class MyClass
{
public:
MyClass() :
// 构造函数中动态分配一个A的实例,并初始化_ptr指向它
_ptr(new A())
{
// ...
}
~MyClass()
{
delete _ptr; // 析构函数中释放_ptr指向的A的实例
// 注意:如果_ptr被浅拷贝到另一个MyClass实例,这将导致问题
}
// 缺少拷贝构造函数和赋值运算符重载,这会导致问题
private:
A* _ptr; // 指向需要管理的A类实例的指针
};
int main()
{
MyClass a1;
MyClass a2 = a1;
// 现在a1._ptr和a2._ptr都指向同一个A的实例
MyClass a3;
a3 = a1;
// 现在a1._ptr、a2._ptr和a3._ptr都指向同一个A的实例
return 0;
}
当main函数结束时,a1、a2和a3的析构函数将按逆序被调用,这将导致同一个A的实例被删除三次,引发未定义行为。
取地址运算符重载
1.const成员函数
将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面。
const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。const修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this
<code>class Date
{
public:
Date(int year, int month, int day) :
_year(year),
_month(month),
_day(day)
{
}
// Print函数被声明为const,表示它不会修改类的任何成员
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2024, 9, 1);
d.Print(); // 调用const成员函数
return 0;
}
2.取地址运算符重载
我们可以对自定义类型使用运算符需要对其进行重载,自然也可以使用&运算符。
例如这样:
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
结束语
这部分内容有点多啊,写的比较长。。。
总之,感谢各位大佬!!!
求点赞收藏评论关注!!!
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。