目录
引言
构造函数
引入构造函数
构造函数的特征
一些细节
析构函数
析构函数的特性
注意事项
拷贝构造函数
书写格式
使用细节
拷贝构造的典型应用场景
运算符重载
意义与格式
注意事项
赋值运算符重载
const成员
两个经典问题
再谈构造函数—初始化列表
注意事项
static成员
概念
特性
两个问题
引言
一个类中什么都没有,简称为空类。
空类中真的什么都没有吗?并不是的,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显示实现,编译器会自动生成的函数叫默认成员函数。
构造函数
引入构造函数
下面通过一段代码引入:
class Date
{
public:
void Init(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()
{
Date d1;
d1.Init(2024, 4, 6);
d1.Print();
Date d2;
d2.Init(2024, 4, 5);
d2.Print();
return 0;
}
对于Date类,我们可以通过公有函数Init来给对象设置日期,但如果每次创建对象时都要调用该函数来设置日期,未免有点麻烦。有没有一种方法,在对象创建时,就将信息设置进去呢?构造函数就是为解决此问题而生的。
构造函数是一个特殊的成员函数,名字与类名相同,在创建类类型的对象时,编译器自动调用,以保证每个数据都有一个合适的初始值,并且在对象的整个生命周期内只调用一次。
需要注意的是,构造函数虽然名称是构造,但是它的主要任务并不是开空间创建对象,而是初始化对象。
构造函数的特征
> 函数名与类名相同
> 无返回值
> 对象实例化时编译器自动调用对应的构造函数
> 构造函数可以重载
示例:
class Date
{
public:
//无参构造函数
Date()
{}
//带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
一些细节
如果我们没有显示的写构造函数,那么编译器会自动生成一个默认构造函数,一旦我们显示的定义了,编译器将不会在自动生成。
不写默认构造:
class Date
{
public:
void Print()
{
cout << _year << _month << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
运行结果是:
输出的均为随机值。
显式写了构造函数,但不是不用传参就能调用的,也会报错,请看下面的代码:
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
内置类型成员在类中声明时可以给默认值(缺省值)。
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 = 1; //在类中声明时可以给缺省值
int _month = 1;
int _day = 1;
};
int main()
{
Date d1(2024, 4, 18);
d1.Print();
return 0;
}
总结:
默认生成的构造函数,对内置类型成员不做处理。内置类型包括int、double及任意类型的指针等。
默认生成的构造函数,对自定义的类型成员,会调用默认构造函数(不用传参就能调用的那个函数)。
内置类型成员在类中声明时可以给缺省值。
析构函数
与构造函数相反,析构函数是完成对象中资源的清理工作。对象在销毁时会自动调用析构函数。
析构函数的特性
> 析构函数函数名在类型前面加上~
> 无参数无返回值
> 一个类只能有一个析构函数,若未显式定义,编译器会自动生成默认构造函数
> 析构函数不能重载
> 对象生命周期结束时,自动调用
> 对于内置类型,自动调用析构函数,对于内置类型不会,因为内置类型中没有资源要清理
注意事项
如果类中没有申请资源,析构函数可以不写,直接使用编译器自动生成的析构函数,比如Date类,
如果涉及到资源的申请,就必须要写析构函数,清理相应资源,否则会导致内存泄漏。
拷贝构造函数
书写格式
只有单个形参,形参类型为类类型对象的引用(一般加const修饰),在用已存在的类类型对象创建新的对象时,编译器自动调用。
//错误写法
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;
}
第一种写法,编译时编译器报错。
因为采用传值方式,会导致无穷递归。 正确的做法应该是要传引用。
还需注意,不要将拷贝构造函数和构造函数混淆,通过它们的参数列表可以很好区分。
使用细节
对于内置类型,可以直接使用编译器自动生成的拷贝构造,编译器生成的拷贝构造函数是按字节序完成拷贝的,这种拷贝方式叫做浅拷贝,或者值拷贝
对于自定义类型,需要调用自己写的拷贝构造,原因如下。
请看下面的例子:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
总结:
如果没有涉及资源的申请,拷贝构造函数是否写都可以,但是,一旦涉及到资源的申请,就必须写拷贝构造函数,否则就是浅拷贝。
拷贝构造的典型应用场景
> 使用已存在对像创建新对象
> 函数参数类型为类类型对象
> 函数返回值类型为类类型对象
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)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2024, 4, 19);
Test(d1);
return 0;
}
自己实现了析构函数释放空间,就需要实现拷贝构造函数。
运算符重载
意义与格式
关键字:operator
格式:返回值类型 operator运算符(参数列表)
注意事项
> 不能通过连接其他符号来创建新的操作符:比如 operator@> 重载操作符必须有一个类类型参数> 用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不能改变其含义> 作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的this> .* :: sizeof ?: . 注意以上 5 个运算符不能重载。
赋值运算符重载
> 参数类型:const T& , 传引用可以提高传参效率。
> 返回值类型: T& , 返回引用可以提高效率,有返回值的目的是为了支持连续赋值。
> 检查是否自己给自己赋值,应避免这种情况发生,因为会降低程序的效率。
> 返回的是 *this,这一点在实现的时候自然就明白了。
class Date
{
public:
Date(int year = 1, 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;
}
//赋值运算符重载
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(2024, 4, 19);
Date d2;
d1 = d2;
return 0;
}
能不能把赋值重载函数定义在全局呢?
请看代码:
class Date
{
public:
Date(int year = 1, 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;
}
/*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;
};
//全局
Date& operator=(Date& d1, Date& d2)
{
if (&d1 != &d2)
{
d1._year = d2._year;
d1._month = d2._month;
d1._day = d2._day;
}
return d1;
}
int main()
{
Date d1(2024, 4, 19);
Date d2;
d1 = d2;
return 0;
}
运行结果:
结论:
赋值运算符重载函数必须是类的成员函数。
原因:赋值重载函数是一个默认成员函数,如果在类中未显示定义,那么编译器会自动生成一个(逐字节拷贝),这就会与我们定义在全局的赋值重载函数冲突,所以赋值运算符重载函数必须为类的成员函数。
和拷贝构造函数一样,如果没有涉及资源管理,那么赋值重载函数是否实现都可以,一旦涉及到资源管理,必须实现赋值重载函数。
const成员
将const修饰的成员函数称为“const成员函数”,const实际上修饰的是隐含的this指针。表明该成员函数中,不能对类的任何成员进行修改。
两个经典问题
1. const 对象可以调用非 const 成员函数吗?不可以,const对象中的成员是不可以修改的,而非const对象中的成员是可以修改的,这是典型的 权限放大 ,是不允许的。2. 非const对象可以调用const成员函数吗?
可以, 非const对象调用const成员函数是一种权限缩小的行为,是允许的。
再谈构造函数—初始化列表
通过一段简单代码引入:
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
在对象实例化时通过上述的构造函数后,类的成员变量有了一个初值,但这并不是初始化,严格来说,应该是赋值,因为在函数体内,可以给同一变量多次赋值,但每一个变量的初始化只有一次,所以,在函数体内给值根本不叫初始化。类的成员变量真正初始化的地方在初始化列表。
初始化列表是构造函数的一部分,由冒号开始,逗号分割,每个成员变量后跟着的括号放初始值或表达式。
Date(int year = 1970, int month = 1, int day = 1)
:_year(year) //初始化列表
,_month(month)
,_day(day)
{}
注意事项
1)每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2)尽量使用初始化列表初始化。初始化列表是所有成员变量(static成员变量除外)定义和初始化的位置,不管是否显式在初始化列表写,都会走初始化列 表进行定义和初始化。如果在初始化列表中给初始值,那么就会用初始值来初始化,如果没给,那么将会用缺省值来初始化。
3)成员变量在初始化列表中的初始化顺序与声明的顺序一致,与初始化列表中的顺序无关。
static成员
概念
声明为static的类成员称为类的静态成员。类的静态成员包含两类:静态成员变量和静态成员函数。
用static修饰的成员变量叫做静态成员变量。
用static修饰的成员函数叫做静态成员函数。
静态成员变量必须在类外进行初始化。上面我们说过,初始化列表是用来给成员变量初始化的,具体的说,是用来给实例化出的对象中的变量初始化的,而静态成员为所有类对象共享,不属于某个具体对象,所以它不是在初始化列表初始化的。
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
static int _m;//静态成员变量的声明
};
int Date::_m = 0;//静态成员变量的初始化
特性
1)静态成员为所有类对象共享,不属于某个具体的对象,存放在静态区。
2)静态成员变必须在类外定义,在类外定义式不加关键字static,类中只是声明。
3)类的静态成员可以用类名::静态成员或对象.静态成员这两种方式来访问。
4)静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
5)静态成员也是类的成员,受访问限定符的限制。
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
static int Get_m()
{
return _m;
}
private:
int _year;
int _month;
int _day;
static int _m;//静态成员变量的声明
};
int Date::_m = 0;//静态成员变量的初始化
int main()
{
cout <<Date::Get_m() << endl;
return 0;
}
可以通过静态成员函数去访问静态成员变量。
两个问题
1)静态成员函数可以调用非静态成员函数吗?
不可以直接调用。请看下面例子:
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
int Add(int x, int y)
{
return x + y;
}
static int Get_m()
{
int ret = Add(1, 2);
return _m;
}
private:
int _year;
int _month;
int _day;
static int _m;//静态成员变量的声明
};
int Date::_m = 0;//静态成员变量的初始化
int main()
{
cout <<Date::Get_m() << endl;
return 0;
}
运行结果:
2)非静态成员函数可以调用静态成员函数吗?
非静态成员函数可以直接调用静态成员函数。
完