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(2023, 6, 5);
d1.Print();
Date d2;
d2.Init(2023, 6, 5);
d2.Print();
return 0;
}
对于这个Date
类,我们使用Init
方法来给对象设置初始,但是每次都需要手动设置,如果对象一旦多了,麻烦不说,还有可能会忘记。C++祖师爷可能被困扰过,于是在C++中引入了构造函数。
构造函数是一个特殊的成员函数,用于在创建对象时进行初始化操作。构造函数的名称与类名相同,并且没有返回类型(包括void)。它在对象被创建时自动调用,用于初始化对象的成员变量和执行其他必要的设置。
构造函数具有以下特点:
构造函数名与类名相同:构造函数的名称必须与其所属类的名称完全相同,包括大小写。
构造函数没有返回类型:与其他函数不同,构造函数没有显式的返回类型,包括void。它们在对象创建时自动调用,无需显式调用。
构造函数在对象创建时被调用:当使用类的对象声明时,构造函数会自动调用,为对象分配内存并初始化其成员变量。
构造函数可以重载:一个类可以有多个构造函数,它们可以具有不同的参数列表(重载),以便在创建对象时提供不同的初始化选项。
class Date
{
public:
//无参数
Date()
{
;
}
//带参数
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;
Date d2(2023, 6, 5);
d1.Print();
d2.Print();
}
Tips:
用无参构造函数创建函数的时候,后面不跟
()
,如果跟了就成了函数声明。
默认构造函数:如果没有定义构造函数,编译器会自动为类生成一个默认构造函数。默认构造函数没有参数,并执行默认的初始化操作。
class Date
{
public:
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
}
运行这段代码,我们发现默认初始化的是随机值。
这其实是属于C++的一个小缺陷。
C++将类型分为了两种:
内置类型:
int
double
char
指针,这些属于语言本身自带的类型自定义类型:用
struct
class
等我们自己定义的类型编译器默认生成的构造函数,不会对内置类型做处理,自定义类型会去调用它自己的默认构造(有些编译器可能会自己处理,但是这并不符合C++的规则)。
所以在一般情况下,如果有内置类型成员,那我们就需要自己写构造函数;
如果全都是自定义类型,则可以考虑让编译器自己生成。
但自定义类型的初始化,最终还是我们自己完成的,这是因为自定义类型再往上追溯也是内置类型。
C++11标准发布的时候,打了一个补丁:在成员声明的时候,可以给缺省值,用来给编译器默认构造函数使用。
class Date { public: void Print() { cout << _year << " " << _month << " " << _day << endl; } private: //不是初始化,给编译器默认构造使用 int _year = 2023; int _month = 1; int _day = 1; };
无参和全缺省的构造函数都是属于默认构造函数,因为它们在对象创建时提供了默认的初始化方式。无参的构造函数适用于不需要任何参数的初始化场景,而全缺省的构造函数适用于所有成员变量都具有默认值的情况。他们都不需要传参。
无参构造函数、全缺省构造函数、编译器默认生成的构造函数,都属于默认构造函数,他们有且只能有一个。
有了自动初始化,那自然是少不了销毁的,这两兄弟一般都是配套的。
析构函数是一种特殊的成员函数,它的功能与构造函数功能相反,用于在对象销毁时进行清理和释放资源的操作。析构函数的名称与类名相同,前面加上波浪号~
,没有参数和返回类型(包括void)。它在对象被销毁时自动调用,用于执行必要的清理操作。
析构函数的名称与类名相同:析构函数的名称必须与其所属类的名称完全相同,并在前面加上波浪号~
,包括大小写。
析构函数没有返回类型:与其他函数不同,析构函数没有显式的返回类型,包括void。它们在对象销毁时自动调用,无需显式调用。
仅能有一个析构函数:一个类只能有一个析构函数。它不允许重载多个析构函数。
析构函数在对象销毁时被调用:当对象的生命周期结束、超出作用域或显式删除对象时,析构函数会自动调用。它用于清理对象所占用的资源。
自定义析构函数:如果不定义析构函数,编译器会生成一个默认的析构函数,执行默认的清理操作。但如果需要执行特定的清理操作或释放动态分配的资源,可以自定义析构函数(类中没有申请资源时,析构函数可以不写;有资源申请时,一定要写,否则会造成资源泄露)。
Ctrl C
(复制)和Ctrl v
(粘贴)是我们十分常用的两个快捷键,而在C++中,有一个类的默认成员函数,也可完成对象的拷贝。
拷贝构造函数是一种特殊的构造函数,用于创建一个对象时,使用同一类的另一个对象的值进行初始化。拷贝构造函数的主要目的是创建一个新的对象,并将已存在对象的值复制到新对象中。
.拷贝构造函数是构造函数的一个重载形式。
拷贝构造函数的参数是对同一类的对象的引用,它用于指定要拷贝的对象。通常使用const
关键字确保被拷贝的对象在拷贝过程中不会被修改。
这里如果直接传值,会导致无穷递归
定义
func1()
和func2()
两个函数,参数类型分别为:自定义类型Date
和内置类型int
通过调试发现,
func1()
,并没有直接进入func1()
函数,而是先进入的拷贝构造,再进入函数本体;而
func2()
则是直接调用函数本身。这是C++的规定:**对于自定义类型,必须调用拷贝构造去完成;**内置类型直接拷贝。
那如果直接传参值,就会出现这样的情况:
如果拷贝构造函数的参数是按值传递的,那么在调用拷贝构造函数时,会创建一个新的对象并传递给参数。但是,这个过程又需要调用拷贝构造函数来创建参数对象,这样就会形成无限递归调用,导致栈溢出或程序崩溃。
不过这里编译器强制检查了,直接报错:
另外,拷贝构造是将原始对象的值复制到新对象中,本身不可改变,所以会使用关键字
const
确保被拷贝的对象在拷贝过程中不会被修改。class Date { public: Date(int year = 2023, 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; } private: int _year; int _month; int _day; }; int main() { Date d1(2023,6,6); Date d2(d1); return 0; }
默认情况下,如果没有为类定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数,该函数会执行逐个成员变量的复制操作。这种操作方式叫做浅拷贝。
需要注意的是,如果类中存在指针类型的成员变量,那么默认的浅拷贝(逐位拷贝)可能会导致问题,因为两个对象会共享相同的指针指向的内存。
如图,
_a
变量是指针类型,进行浅拷贝的时候,两个对象指向了同一块空间。在这种情况下,通常需要自定义拷贝构造函数,进行深拷贝(分配新的内存并复制数据)以避免潜在的问题。class Stack { public: Stack(int capacity = 4) { _capacity = capacity; _top = 0; cout << "Stack()" << endl; _a = (int*)malloc(sizeof(int) * capacity); if (nullptr == _a) { perror("malloc申请空间失败"); return; } } Stack(const Stack& st) { _capacity = st._capacity; _top = st._top; _a = (int*)malloc(sizeof(int) * st._capacity); if (nullptr == _a) { perror("malloc申请空间失败"); return; } memcpy(_a, st._a, sizeof(int) * st._top); } ~Stack() { cout << "~Stack()" << endl; free(_a); _a = nullptr; _capacity = _top = 0; } private: int* _a = nullptr; int _top = 0; int _capacity; }; int main() { Stack st1; Stack st2(st1); return 0; }
C++中,内置类型可以直接比较,但是对于自定义类型,想要比较,需要我们自己写出对应的方法。
class Date
{
public:
Date(int year = 2023, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool Less(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year && _month < d._month)
return true;
else if (_year == d._year && _month == d._month && _day < d._day)
return true;
else if (_year == d._year && _month == d._month && _day == d._day)
exit(-1);
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//内置类型
int a = -1;
int b = 1;
cout << (a > b) << endl;
//内置类型
Date d1(2023,6,7);
Date d2(2023, 6, 6);
cout << d1.Less(d2) << endl;
return 0;
}
这种方式显然不是很直白,于是C++引入了运算符重载:在类中重新定义已有
的运算符,使其能够对类的对象进行操作。
运算符重载函数的命名形式为 operator<运算符>
,例如这里比较是否小于operator<
Tips:
- 运算符重载应该遵循对操作数的预期语义
- 要重载一个运算符,需要在类中定义一个对应的成员函数
.*
::
sizeof
?:
.
这5个运算符不能重载
class Date
{
bool operator<(const Date& d)
{
//...
}
//...
}
int main()
{
//...
Date d1(2023,6,7);
Date d2(2023, 6, 6);
//"<<"有优先级高于"<",所以这里要加上括号
cout << (d1<d2) << endl;
return 0;
}
前面所了解的拷贝构造函数,是对于一个对象去初始化创建另一个对象。而赋值运算符,用于已存在的两个对象之间的赋值。
拷贝构造的本质是一个构造函数,而赋值运算符本质上是一个运算符重载。
class Date
{
public:
Date(int year = 2023, 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;
}
//运算符重载
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, 6, 7);
// 初始化 -- 构造函数
Date dd1(d1);
Date d2, d3, d4;
// 赋值 -- 运算符重载
d2 = d1;
d4 = d3 = d2;
}
class Date
{
public:
//构造函数
Date(int year = 2023, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//前置++
Date& operator++()
{
*this += 1;
return *this;
}
//后置++
Date operator++(int)
{
Date tmp(*this);
tmp += 1;
return tmp;
}
private:
int _year;
int _month;
int _day;
};
由于前置++和后置++的运算符都是++
,为了以示区分,C++在后置++重载时加入了一个int
类型的参数,不过是使用这个函数的时候,不需要传递,编译器回自动判断(前置–、后置–也是同理)。
当然了这里前置和后置的返回类型不一样,前置的返回的引用,后置返回的是临时变量。
所以当我们使用自定义类型的时候,使用前置,这样效率会稍高一点。
在C++中,const
成员是指在类中声明为const的成员变量或成员函数。
const
成员变量:在类中声明为const的成员变量。它们必须在类的构造函数初始化列表中进行初始化,并且不能在类的任何成员函数中修改。const成员变量可以是基本数据类型(例如int、float等)或自定义类型。const成员变量在对象创建时就被初始化,并且在对象的整个生命周期中保持不变。
const
成员函数:const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。const成员函数通过在函数声明的末尾加上const关键字来标识。
const成员函数可以重载非const成员函数,但非const成员函数不能重载const成员函数。这是因为权限可以被缩小,但是不能被放大。
如果我们设置函数的时候,该函数不想改变成员变量,我们就可以使用const
来修饰,这可以使我们的代码更加健壮。
这两个不常用,一般不会重新定义,编译器会默认生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ;
int _month ;
int _day ;
};
本篇主要介绍了C++的几个默认成员函数,可以把他们理解为C++的“贵族”,因为他们都是祖师爷钦点的。
那么本期的分享就到这里啦,我们下期再见,如果还有下期的话。