1. 类的6个默认构造函数
C++ 编译器会为每个类自动生成以下6个默认成员函数(除非显式定义)。如果用户没有定义这些函数,编译器会生成默认实现:
A obj; // 自动调用构造函数
A* ptr = new A(); // 自动调用构造函数
void
也没有)。
class A {
public:
A() {
// 默认构造函数
std::cout << "Default constructor called" << std::endl;
}
};
class A {
public:
int x, y;
A(int a, int b) : x(a), y(b) {
std::cout << "Parameterized constructor called" << std::endl;
}
};
A(const A& other)
,用于拷贝已有对象。class A {
public:
A(const A& other) {
std::cout << "Copy constructor called" << std::endl;
}
};
class A {
public:
A(A&& other) noexcept {
std::cout << "Move constructor called" << std::endl;
}
};
构造函数的初始化列表可以用于高效地初始化成员变量,特别是当成员是类类型或常量时。它比在构造函数体内赋值更优,因为成员变量会在进入构造函数体之前被初始化。
class A {
public:
int x;
const int y;
A(int a, int b) : x(a), y(b) { // 初始化列表
std::cout << "Constructor with initializer list called" << std::endl;
}
};
优势:
const
成员或引用类型成员的初始化,因为它们只能被初始化一次。~
符号,且不接受参数也没有返回值。
delete
),析构函数会被触发。
class A {
public:
~A() {
std::cout << "Destructor called" << std::endl;
}
};
int main() {
A obj; // 栈上对象,超出作用域时析构
A* ptr = new A(); // 堆上对象
delete ptr; // 显式调用析构函数
}
new
),析构函数中需要释放这些资源,避免内存泄漏。class A {
private:
int* data;
public:
A() {
data = new int[10]; // 动态分配资源
}
~A() {
delete[] data; // 释放资源
std::cout << "Destructor called" << std::endl;
}
};
总结:
拷贝构造函数是用来通过已有对象来初始化新对象的特殊构造函数。它的主要作用是复制一个对象的所有成员,从而创建一个新的与原对象相同的对象。
在C++中,拷贝构造函数的定义通常如下:
class A {
public:
A(const A& other); // 拷贝构造函数
};
const
引用,避免在拷贝时对原对象的修改。当通过另一个对象来初始化新对象时,拷贝构造函数会被调用。例如:
A obj1; // 默认构造函数
A obj2 = obj1; // 调用拷贝构造函数
当对象被按值传递给函数时,也会调用拷贝构造函数。
当函数按值返回对象时,也会调用拷贝构造函数。
以下是一个简单的类示例,演示了拷贝构造函数的工作方式:
#include <iostream>
using namespace std;
class A {
public:
int* data;
// 构造函数
A(int val) {
data = new int(val);
cout << "Constructor called, value: " << *data << endl;
}
// 拷贝构造函数(深拷贝)
A(const A& other) {
data = new int(*(other.data)); // 分配新内存,并复制数据
cout << "Copy Constructor called, value: " << *data << endl;
}
// 析构函数
~A() {
delete data;
cout << "Destructor called, value deleted" << endl;
}
};
int main() {
A obj1(10); // 调用普通构造函数
A obj2 = obj1; // 调用拷贝构造函数(深拷贝)
return 0;
}
输出结果:
Constructor called, value: 10
Copy Constructor called, value: 10
Destructor called, value deleted
Destructor called, value deleted
说明:
obj1
使用普通构造函数创建,分配了动态内存并存储值10。obj2
是通过拷贝构造函数创建的,它分配了新的内存,并将obj1
的数据复制给它(深拷贝)。int
,是因为你希望通过传递一个整数值来初始化对象。int*
指针,是为了在对象内部动态管理内存,提供深拷贝和析构的能力。这样每个对象都有自己的独立数据,避免了指针共享带来的潜在问题。运算符重载(Operator Overloading)是C++的一种特性,它允许你为类定义或重载运算符,以便让这些运算符能够处理自定义数据类型。运算符重载使得自定义类可以使用与内置类型相同的运算符进行操作,提高了代码的可读性和一致性。
在C++中,赋值运算符的重载允许你定义如何将一个对象的值赋给另一个对象。其标准格式是:
class ClassName {
public:
// 赋值运算符重载
ClassName& operator=(const ClassName& other);
};
详细解释:
ClassName&
),以支持链式赋值(例如 a = b = c;
)。const
引用作为参数,这个引用指向要赋值的对象(const ClassName& other
)。使用const
是为了保证赋值操作不会修改源对象。a = a
),需要先检查赋值的对象与当前对象是否相同。*this
),使得赋值运算符可以支持连续赋值(如 a = b = c
)。以下是一个类的赋值运算符重载的示例,包含指针的深拷贝操作:
#include <iostream>
using namespace std;
class A {
public:
int* data;
// 构造函数
A(int val) {
data = new int(val); // 动态分配内存,并用 val 初始化
cout << "Constructor called, value: " << *data << endl;
}
// 拷贝构造函数(深拷贝)
A(const A& other) {
data = new int(*(other.data));
cout << "Copy Constructor called, value: " << *data << endl;
}
// 赋值运算符重载(深拷贝)
A& operator=(const A& other) {
if (this == &other) { // 自我赋值检查
return *this; // 如果是自我赋值,直接返回当前对象
}
delete data; // 释放当前对象的已有资源
data = new int(*(other.data)); // 分配新内存并复制数据
cout << "Assignment Operator called, value: " << *data << endl;
return *this; // 返回当前对象的引用
}
// 析构函数
~A() {
delete data; // 释放动态分配的内存
cout << "Destructor called, value deleted" << endl;
}
};
int main() {
A obj1(10); // 调用构造函数
A obj2(20); // 调用构造函数
obj2 = obj1; // 调用赋值运算符重载
A obj3 = obj1; // 调用拷贝构造函数
return 0;
}
输出结果:
Constructor called, value: 10
Constructor called, value: 20
Destructor called, value deleted
Assignment Operator called, value: 10
Copy Constructor called, value: 10
Destructor called, value deleted
Destructor called, value deleted
Destructor called, value delete
详细解释:
A obj1(10)
和 A obj2(20)
分别为两个对象分配动态内存,并初始化它们的值为 10
和 20
。obj2 = obj1
会调用赋值运算符重载。首先进行自我赋值检查,接着释放 obj2
原有的动态内存(值为 20
),然后为 obj2
分配新内存并将 obj1
的值 10
复制过来。A obj3 = obj1
会调用拷贝构造函数,通过深拷贝为 obj3
分配独立的内存并复制 obj1
的值。main
函数结束时,所有对象(obj1
、obj2
、obj3
)都会调用析构函数,释放各自的动态内存。自我赋值的处理:
在赋值运算符重载中,检查自我赋值是非常重要的。假设不进行自我赋值检查,可能会出现如下情况:
obj1 = obj1; // 自我赋值
this == &other
,delete data
会释放 obj1
的动态内存,而接下来的 data = new int(*(other.data))
会访问已被释放的内存,导致未定义行为。为什么返回 *this?
返回 *this
(即当前对象的引用)是为了支持连续赋值操作。例如:
A obj1(10), obj2(20), obj3(30);
obj3 = obj2 = obj1; // 支持链式赋值
通过返回当前对象的引用,赋值运算符可以被连续调用,从而支持链式操作。
总结:
+=
运算符重载+=
运算符用于将指定的值加到当前对象上。它通常会修改当前对象的状态,并返回对象本身的引用。
Date& operator+=(int days) {
// 增加指定的天数
for (int i = 0; i < days; ++i) {
incrementDay(); // 增加一天
}
return *this; // 返回当前对象的引用,以支持链式操作
}
+
运算符重载+
运算符用于生成一个新的对象,该对象的值是当前对象加上指定的天数。它通常不会修改当前对象,而是返回一个新的对象。
Date operator+(int days) const {
Date temp = *this; // 创建当前对象的副本
temp += days; // 使用 `+=` 运算符增加天数
return temp; // 返回新的日期对象
}
++
运算符重载Date& operator++() {
incrementDay(); // 增加一天
return *this; // 返回当前对象的引用
}
++
运算符重载Date operator++(int) {
Date temp = *this; // 保存当前日期
incrementDay();
return temp; // 返回原日期
}
前置++和后置++的重载区别:
this指向的对象函数结束后不会销毁,因此可以使用引用返回
前置++和后置++都是一元运算符,为了使前置++和后置++能正确重载,C++规定:后置++运算符重载时多增加一个int类型的参数,但调用函数时该参数不用传递,由编译器自动传递。后置++要返回旧值,因此需要创建临时变量存储++前的值,最终返回的也是旧值,因为旧值存放在临时变量中,因此只能传值返回,不能引用返回。
<<
运算符重载在C++中,通常我们会为类重载输入/输出流运算符(<<
)以实现自定义的输入和输出操作。对于日期类来说,你可能希望通过重载流插入运算符(<<
)来格式化日期的输出。然而,这个重载函数通常不能作为类的成员函数,以下是一些原因:
流插入运算符<<
的左操作数通常是标准输出流对象(如std::ostream
)。如果你将<<
作为类的成员函数来重载,那么左操作数将隐式绑定为类的实例,即第一个操作数必须是类的对象。但在我们通常的用法中,左操作数是std::ostream
,而不是类的实例。
例如,假设你有一个Date
类:
class Date {
int year, month, day;
// 以成员函数形式的流插入运算符重载
std::ostream& operator<<(std::ostream& out) {
out << year << "年" << month << "月" << day << "日";
return out;
}
};
上面的代码会有问题,因为<<
的左操作数必须是std::ostream
,而类成员函数的隐含调用this指针的方式会将Date
对象作为左操作数,这与标准使用方式相冲突。以下是示例代码:
#include<iostream>
using namespace std;
class Date {
int year, month, day;
// 以成员函数形式的流插入运算符重载
public:
ostream& operator<<(std::ostream& out) {
out << year << "年" << month << "月" << day << "日";
return out;
}
Date(int year, int month, int day) : year(year), month(month), day(day) {}
};
int main()
{
Date d(2023, 9, 9);
// cout << d; // 这里左操作数是std::cout
d << cout; // 正确操作,但是与标准操作不符
return 0;
}
解决方案:我们通常将<<
运算符重载为非成员函数,并且通常定义为友元函数以便它能访问类的私有成员。
class Date {
private:
int year;
int month;
int day;
friend ostream& operator<<(ostream& out, const Date& d);
};
ostream&
函数返回一个对输出流 (ostream
) 的引用。这样做的目的是为了支持连续的流操作符链式调用。ostream& out
: 这是输出流的引用。通常是std::cout
或其他ostream
类型的流,如文件流ofstream
。const Date& d
: 这是一个常量引用类型的Date
对象。使用const
确保函数在输出时不会修改传入的Date
对象,使用引用避免传递对象时的拷贝,提高效率。>>
运算符重载和<<
一样需要定义友元函数以便它能访问类的私有成员,其他注意问题与<<
同理解决,建议模仿上述代码自行理解。
返回类型: istream&
函数返回一个对输入流 (istream
) 的引用。这使得可以支持链式输入操作。例如,允许这样的代码:
std::cin >> d1 >> d2;
返回流的引用使得多个对象可以通过单个流操作符连续读取。
参数:
istream& in
: 输入流的引用,通常是 std::cin
或其他 istream
类型的输入流(如文件流 std::ifstream
)。Date& d
: 一个传递进来的 Date
对象的引用。因为这是一个非 const
引用,意味着可以在函数内对 Date
对象进行修改(即从输入流中读取到的数据将赋值给这个 Date
对象)。#include <iostream>
#include <stdexcept>
using namespace std;
class Date {
private:
int year;
int month;
int day;
// 声明友元函数,方便外部重载的流插入与流提取函数访问私有成员变量
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
// 检查是否是闰年
bool isLeapYear(int year) const {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
// 检查日期是否合法
bool isValidDate(int year, int month, int day) const {
if (month < 1 || month > 12) return false;
if (day < 1 || day > daysInMonth(month, year)) return false;
return true;
}
// 获取指定月份的天数
int daysInMonth(int month, int year) const {
// 每个月的天数,2月在闰年时会有29天
static const int daysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 && isLeapYear(year)) {
return 29; // 闰年2月有29天
}
return daysInMonth[month - 1]; // 返回非闰年的月份天数
}
// 将日期递增一天
void incrementDay() {
++day; // 天数加1
if (day > daysInMonth(month, year)) { // 如果天数超出该月的天数
day = 1; // 设为1号
++month; // 月份加1
if (month > 12) { // 如果月份超出12
month = 1; // 设为1月
++year; // 年份加1
}
}
}
public:
// 构造函数,初始化日期
Date(int year, int month, int day) : year(year), month(month), day(day) {
if (!isValidDate(year, month, day))
throw invalid_argument("Invalid date");
}
// 前置 ++ 运算符重载
Date& operator++() {
incrementDay(); // 递增一天
return *this; // 返回当前对象的引用,以支持链式操作
}
// 后置 ++ 运算符重载
Date operator++(int) {
Date temp = *this; // 创建当前对象的副本
incrementDay(); // 递增一天
return temp; // 返回副本,表示递增前的日期
}
// += 运算符重载
Date& operator+=(int days) {
for (int i = 0; i < days; ++i) {
incrementDay(); // 增加指定的天数
}
return *this; // 返回当前对象的引用,以支持链式操作
}
// + 运算符重载
Date operator+(int days) const {
Date temp = *this; // 创建当前对象的副本
temp += days; // 使用 += 运算符增加天数
return temp; // 返回新的日期对象
}
};
// << 运算符重载
ostream& operator<<(ostream& out, const Date& d)
{
out << d.year << "年" << d.month << "月" << d.day << "日" << endl;
return out;
}
// >> 运算符重载
istream& operator>>(istream& in, Date& d)
{
in >> d.year >> d.month >> d.day;
if (!d.isValidDate(d.year, d.month, d.day)) {
in.setstate(ios::failbit); // 输入非法时设置错误标志
throw invalid_argument("Invalid date input");
}
return in;
}
// 测试
int main() {
Date d(2024, 2, 28); // 创建一个日期对象
cout << d; // 打印: 2024年2月28日
++d; // 前置 ++ 操作
cout << d;; // 打印: 2024年2月29日 (2024年是闰年)
d++; // 后置 ++ 操作
cout << d; // 打印: 2024年3月1日
d += 30; // 增加30天
cout << d; // 打印: 2024年3月31日
Date d2 = d + 1; // 使用 + 运算符
cout << d2; // 打印: 2024年4月1日
cin >> d; // 输入2005 3 20
cout << d; // 打印:2005年3月20日
return 0;
}
.
(成员访问符).*
(成员指针访问符)::
(作用域解析符)?:
(条件运算符)sizeof
(字符长度运算符)const
成员函数const
关键字我们知道,在C++中,若一个变量声明为const
类型,则试图修改该变量的值的操作都被视编译错误。例如,
const int day = 5;
day = 15; // 错误,day是常量,不能被修改
// a拷贝给b,b的改变不影响a,不涉及权限放大问题
const int a = 10;
int b = a;
// b是a的别名,b的改变会影响a,设计权限放大
const int a = 10;
int& b = a;
// 跟引用类似,涉及权限放大
const int a = 10;
int* pb = &a;
const
的语法示例在C++中,const
成员函数是指那些不修改对象状态(即不修改对象成员变量)的成员函数。这类函数的声明在函数名后面加上const
关键字,用来保证这个函数不会修改任何非const
的成员变量。
特点:
const
成员函数中,不能修改任何非const
成员变量。const
成员函数: 在const
成员函数中,您只能调用其他const
成员函数,不能调用非const
成员函数,因为非const
函数可能修改对象的状态。const
对象: 如果对象是const
类型,则只能调用该类中的const
成员函数,否则编译器会报错。语法:
class MyClass {
public:
int getValue() const { // 这是一个 const 成员函数
return value;
}
void setValue(int val) { // 非 const 成员函数
value = val;
}
private:
int value;
};
说明:
getValue
函数被声明为const
,这意味着它不能修改MyClass
对象的任何成员变量。setValue
不是const
,因此它可以修改成员变量value
。const成员函数的作用:
const
对象可以调用这些函数,保持接口的一致性。示例:
int main() {
const MyClass obj; // 定义一个 const 对象
obj.getValue(); // 只允许调用 const 成员函数
// obj.setValue(5); // 错误:不能调用非 const 成员函数
}
总结:
const
对象不可以调用非·成员函数(权限放大不允许)const
对象可以调用const
成员函数(权限缩小允许)const
成员函数内不可以调用其他的非const
成员函数(权限放大不允许)const
成员函数(权限缩小允许)*this
对象数据的,这些成员函数都应该加const
修饰const
取地址操作符重载这两个运算符一般不需要重载,使用编译器默认生成取地址的重载即可。
//日期类
class Date
{
public:
//取地址重载
Date* operator&()
{
return this;
}
//const修饰的取地址操作符重载,给const对象调用
const Date* operator&() const
{
return this;
}
private:
int _year;
int _month;
int _day;
};