目录
类的默认成员函数
- 对于类,如果我们不自动实现,编译器会自动生成6个默认成员函数:构造函数、析构函数、拷贝构造函数、赋值操作符重载、取地址和const取地址操作符重载、const成员函数。
1.构造函数
- 构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。构造函数的主要任务并不是开空间创建对象,而是初始化对象。
特性
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
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; //调用无参构造函数
d1.Print();
Date d2(1, 1, 1);//调用带参构造函数
d2.Print();
return 0;
}
- 编译器默认生成的构造函数、无参的构造函数和全缺省的构造函数都是默认构造函数,并且默认构造函数只能有一个(否则调用时会出现问题)。
class Date
{
public:
//系统默认生成的构造函数
//...
//无参构造函数
//Date()
//{}
//全缺省构造函数
Date(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;
};
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦显式定义编译器将不再生成。
class Date
{
public:
//系统默认生成的构造函数
//...
//无参构造函数
//Date()
//{}
//全缺省构造函数
//Date(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.Print();
}
注:如果只定义了其他构造函数,而没有定义无参或全缺省构造函数,编译器也不会生成默认构造函数,那么这个类就没有默认构造函数,初始化时会出现问题。
- 编译器生成的默认构造函数对于内置类型不做处理(是随机值),对于自定义类型则调用自定义类型的构造函数。
C++把类型分成内置类型(基本类型)和自定义类型,内置类型就是语言提供的数据类型(int,char,指针…),自定义类型就是我们定义的类、结构体等类型。
初始化列表
- 虽然在构造函数调用之后,对象中已经有了一个初始值,但并不是对象的成员变量初始化的,而是在函数体中赋的初值。因为初始化只能初始化一次,而构造函数体内可以多次赋值。为了解决引用类似只能初始化赋值的情况,使用初始化列表。
- 格式:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟
一个放在括号中的初始值或表达式。
// 全缺省的构造函数
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
- 注:
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
类中包含引用成员变量,const成员变量,自定义类型成员(且该类没有默认构造函数时)时,必须放在初始化列表位置进行初始化。
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
2.析构函数
- 与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
特性
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
- 编译器自动生成的析构函数对于内置类型成员不做处理,最后系统直接将其内存回收;对于自定义类型则调用自定义类型的析构函数。
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
class Date
{
public:
//析构函数
~Date()
{
//释放堆申请的空间...
//这里没有就不需要
}
private:
int _year;
int _month;
int _day;
};
3.拷贝构造函数
- 使用一个已知对象的数据成员值拷贝给正在创建的另一个同类的对象。
特性
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用。因为传值方式会引发无穷递归调用。
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对于内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
4.运算符重载
- C++ 为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名:关键字operator后面+需要重载的运算符符号。
函数原型:返回值类型 operator 操作符(参数列表)
operator== // 函数名
bool operator==(const Date& d1, const Date& d2); //全局函数声明
特性
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的==,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this
class Date
{
public:
// d1 == d2; 调用时==会被看作调用d1的成员函数,编译器会转换成 d1.operator==(d2)
//也可以直接写d1.operator==(d2),d1 == d2是编译器做的特殊处理
bool operator==(const Date& d)
{
//this 是d1, d 是d2
if (this._year == d._year && _month == d._month && _day == d._day)
return true;
return false;
}
// private:
int _year;
int _month;
int _day;
};
// d1 == d2; 编译器会转换成 operator==(d1, d2), 两种写法都可以
bool operator==(const Date& d1, const Date& d2); //全局函数声明
- 前置++和后置++重载
由于前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载,所以C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
class Date
{
public:
// 前置++声明
Date& operator++();
// 后置++声明
Date operator++(int);
// 前置++
Date& operator++()
{
*this += 1;
return *this;
}
// 后置++
Date operator++(int)
{
Date d(*this);
*this += 1;
return d;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main()
{
Date d1;
//会被转换成 d1.operator++(值)调用时向内置类型一样使用即可,不需要传值,编译器会自动传值
d1++;
//会被转换成 d1.operator++()调用时向内置类型一样使用即可
++d1;
}
- .(点运算符) ::(域运算符) .*(点星运算符,) ?:(条件运算符) sizeof 以上5个运算符不能重载。
赋值运算符重载
- 使用一个对象对另一个同类的对象赋值。
- 格式:
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
// 赋值运算符重载
// d2 = d3 -> d2.operator=(d3)
Date& operator=(const Date& d)
{
if (this != &d) //检测是否自己给自己赋值,检查的是类的地址
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
- 如果不显式实现,编译器会默认生成一个,对于内置类型成员变量是以值的方式逐字节拷贝,而自定义类型成员变量会调用对应类的赋值运算符重载完成赋值。所以不能在类外重载全局的赋值运算符,会和类中默认生成的冲突。
5.const成员
- 将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
有的情况下不希望成员函数可以修改成员变量或者本身就是const修饰的对象,就需要对 this 指针加上const,由于this是隐式传的, 所以在成员函数的参数列表后加const。
//打印
void print() const //实际上修饰的是 this 指针 ->> void print(const Date* this)
{
cout << _year <<' '<< _month <<' '<< _day << endl;
}
6.取地址及const取地址操作符重载
- 这两个默认成员函数一般不用重新定义 ,编译器默认生成的足够使用。
//取地址
Date* operator&()
{
return this;
}
//取const对象的地址
const Date* operator&() const
{
return this;
}
- 除非特殊情况,才需要重载,不想让别人取地址类的或指定地址的。
const Date* operator&() const
{
return nullptr;
}
7.static成员
- 声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
- 特性:
静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
静态成员函数没有隐藏的this指针,不能访问任何非静态成员
静态成员也是类的成员,受public、protected、private 访问限定符的限制
class Date
{
//private:
public:
static int _count; //
};
int Date::_count = 0; //类外初始化
int main()
{
Date::_count++; //不需要定义对象就可以访问(受访问限定符限制)
Date d;
d._count++; //也可以使用对象访问(受访问限定符限制)
return 0;
}
8.友元
- 一个类a的成员可以被另一个类b或者全局函数c访问,那么类b和全局函数c就是类a的友元类和友元函数。
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用
友元函数
- 一个全局函数需要访问类的成员,使用友元解决
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在
类的内部声明,声明时需要加friend关键字。
例如现在要重载operator<<,但this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。
class Date
{
public:
// cout << d; //这里只有 d << cout 这样才会正常运行
/*ostream& operator<<(ostream& _cout) //重载符号默认符号左侧是第一参数,也就是this指针
{
_cout << _year << "-" << _month << "-" << _day;
return _cout;
}*/
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
cout << d << endl;
return 0;
}
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d); //友元的声明
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
int main()
{
Date d;
cout << d << endl;
return 0;
}
- 特性:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
友元类
- 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
-
- 特性:
友元关系是单向的,不具有交换性。(类a声明 类b是其友元类,那么类b可以访问类a的成员变量, 类a不可以访问类b的成员变量)
友元关系不能传递。(c是b的友元, b是a的友元,如果a没有声明c是其友元,那么c依旧不是a的友元类)
友元关系不能继承。
- 特性:
9.内部类
- 如果在一个类中定义另一个类(一个类中包含了另一个类),这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类。所以不能通过外部类的对象去访问内部类的成员,外部类对内部类没有任何的访问权限。
但内部类天生就是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员。 - 特性:
内部类可以定义在外部类的public、protected、private都是可以的。
注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
sizeof(外部类)=外部类,和内部类没有任何关系
class A
{
class B
{
private:
int _b;
};
private:
int _a;
};
int main()
{
A a1;
int a1_sz = sizeof(a1); //这里a1对象的大小是4字节
return 0;
}
10.匿名对象
- 一个对象在创建时只使用一次的时候,可以使用匿名对象。
- 特性:
匿名对象的特点不用取名字,类直接加括号就行。
匿名对象的生命周期只有一行,创建使用后就析构了。
class Date
{
public:
//全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
vector<Date> v1;
Date d1(2023,11,6);
v1.push_back(d1);
v1.push_back(Date(2023,11,6)); // 这里使用匿名函数就很方便
return 0;
}