专栏:C/C++
个人主页:HaiFan.
专栏简介:本章为大家带来C++类和对象相关内容。
类和对象下
- 类的默认成员函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 运算符重载
- const成员
- 再谈构造函数
- 构造函数体赋值
- 初始值列表
- explicit关键字
- static成员
- 友元
- 内部类
- 匿名对象
- 拷贝对象时一些编译器优化
类的默认成员函数
默认成员函数是指在定义类时如果没有显式声明该成员函数,编译器会自动生成默认实现的成员函数。
构造函数
构造函数是一种特殊的成员函数,它没有返回值。它的作用是在创建对象的时候初始化成员变量,如果没有显示的定义构造函数,编译器会隐式的生成一个默认构造函数。如果定义了构造函数。默认的就不会在自动生成。
特点:
- 函数名与类名相同
- 没有返回值
- 创建对象的过程中编译器自动调用对应的构造函数
- 可以重载
- 在对象整个生命周期中只调用一次
class Date
{
public:
Date()
{
//无参构造函数
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
//带参构造函数
}
void GetDate()
{
cout << _year << ':' << _month << ':' << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date one;//调用无参
one.GetDate();
Date two(2023, 5, 6);//调用有参
two.GetDate();
//如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
return 0;
}
输出结果 -858993460:-858993460:-858993460 2023:5:6
第一个结果是个随机值,为什么呢?构造函数不是会初始化吗?
当类中没有定义构造函数或者定义了构造函数但没有对成员变量进行初始化的时候,前者编译器会自动生成默认构造函数,但是这个构造函数不会对类中的内置类型进行初始化,依旧是随机值。如果是自定义类型,默认生成的构造函数会进行初始化,前提是自定义类型中定义了构造函数。
#include <iostream>
using namespace std;
class Stack
{
private:
int* _a;
int _capacity;
int _t;
public:
Stack(int capacity = 4)
{
_a = new int[capacity];
_capacity = capacity;
_t = -1;
}
void push(int x)
{
_a[++_t] = x;
}
int top()
{
return _a[_t];
}
};
class MyQueue
{
private:
Stack q1;
Stack q2;
};
int main()
{
MyQueue a1;
return 0;
}
上面的代码中,MyQueue中没有构造函数,但是编译器会自动生成从而完成初始化。
如果把Stack类中的构造函数给注释了,q1和q2还是会出现随机值。
C++11中针对内置类型给成员不初始化的缺点,打了补丁。
内置类型的成员变量在类中声明时可以给定默认值,如果创建该类的对象时没有提供该成员变量的值,则该成员变量将被初始化为默认值。
#include <iostream>
using namespace std;
class Date
{
public:
//Date()
//{
//
//}
//Date(int year, int month, int day)
//{
// _year = year;
// _month = month;
// _day = day;
//}
void GetDate()
{
cout << _year << ':' << _month << ':' << _day << endl;
}
private:
int _year = 2023;
int _month = 05;
int _day = 06;
};
int main()
{
Date one;
one.GetDate();
return 0;
}
输出结果 2023:5:6
注:需要注意的是,只有在声明类的同时给成员变量赋予初始值,它们才会被初始化。如果你使用了构造函数来初始化内置类型的成员变量,则默认值将不起作用。
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且构造函数只能有一个,如果同时出现,会出现 重载不明确的报错
析构函数
析构函数是在对象被销毁时(生命周期结束时)自动调用。与构造函数不同,析构函数没有任何参数,没有返回值,其名称由类名前加上 ~
组成。
主要作用:
用于清理和释放对象占用的资源,如动态内存开辟的空间,打开的文件等,如果忘记释放的话,会造成内存泄露等问题。
一个类只能有一个析构函数,若未显示定义,系统会自动生成默认的析构函数
析构函数不能重载
内置成员类型,销毁时不需要资源清理,最后系统直接将其内存回收即可
class Data
{
private:
int _day;
int _month;
int _year;
public:
Data(int day, int month, int year)
{
cout << "--------Data--------" << endl;
_day = day;
_month = month;
_year = year;
}
~Data()
{
cout << "--------~Data-------" << endl;
}
};
int main()
{
Data a(8, 5, 2023);
return 0;
}
输出结果--------Data-------- --------~Data-------
如果是自定义类型
class Time
{
private:
int _a;
public:
~Time()
{
cout << "--------~Time-------" << endl;
}
};
class Data
{
private:
int _day;
int _month;
int _year;
public:
Time _t;
};
int main()
{
Data a;
return 0;
}
a销毁时,要将其内部包含的Time类的 _t对象给销毁,所以会调用Time类的析构函数,但是main函数中是不能直接调用Time类中的析构函数的,编译器会调用Data类的析构函数,因为没有写,会自动给Data类生成一个默认的析构函数,然后在其内部调用Time。
如果类中没有申请资源,析构函数可以不屑,直接使用编译器生成的默认析构函数就行。
拷贝构造函数
拷贝构造函数用于将一个对象复制到另一个对象中,拷贝构造函数只有一个参数,通常是一个引用类型的对象,这个参数指定了需要被复制的对象。这个函数不需要自己去调用,在复制对象的过程中编译器会自动调用。
className(const className& other)
{
//拷贝内容
}
拷贝构造函数的参数是一个常量引用,这个引用指向需要复制的对象。如果使用传值的方式,编译器会直接报错。(首先在C++中,内置类型的传参是直接赋值,而自定义类型的传参则是会进去类中的拷贝构造函数进行一个拷贝,如果拷贝构造函数中是用的传值,就会造成无限递归)
class Date
{
private:
int _year;
int _day;
int _month;
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//Date(Date other)错误写法
Date(const Date& other)
{
_year = other._year;
_month = other._month;
_day = other._day;
}
};
int main()
{
Date d(2023,5,9);
Date d1(d);
return 0;
}
若没有显示定义,则编译器会生成默认的拷贝构造函数,默认的拷贝构造函数是按照字节完成拷贝的,这种拷贝叫做浅拷贝,或者值拷贝。
class Date
{
private:
int _year;
int _day;
int _month;
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
};
int main()
{
Date d(2023, 5, 9);
Date d1(d);
return 0;
}
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
像上面的代码,全都是内置类型的,不需要自己去实现拷贝构造函数,如果是有动态内存的,资源申请之类的,则需要自己写拷贝构造函数。
class Stack
{
private:
int* _a;
int t;
int _capacity;
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _a)
{
perror("malloc fail");
exit(-1);
}
_capacity = capacity;
t = -1;
}
void push(int x)
{
_a[++t] = x;
}
int top()
{
return _a[t];
}
~Stack()
{
free(_a);
_a = nullptr;
}
};
int main()
{
Stack stk1;
Stack stk2(stk1);
return 0;
}
这个代码就是一个错误的示范,因为Stack类涉及到了动态内存分配,并且类中并没有显式定义拷贝构造函数,这个时候编译器会自定生成一个默认的拷贝构造函数,编译器生成的这个,是浅拷贝。
通过监视,可以发现,stk1和stk2的栈数组指向的是同一块空间,所以在释放资源的时候,当其中一个先释放之后,另一个在释放,就会造成错误,同一块空间不能连续释放两次。
运算符重载
这里先写了一个日期类
class Date
{
public:
Date(int year,int month,int day);
Date(const Date& other);
private:
int _year;
int _day;
int _month;
};
Date::Date(const Date& other)
{
_year = other._year;
_month = other._month;
_day = other._day;
}
Date::Date(int year,int month, int day)
{
_year = year;
_day = day;
_month = month;
}
int main()
{
Date d(2023, 5, 9);
Date d1(2024, 5, 9);
cout << (d1 > d) << endl;
return 0;
}
实现一个日期类,很简单,如果让实现一个比较日期大小的函数呢?
bool Greater(const Date& d, const Date& d1)
{
if (d._year > d1._year)
{
return true;
}
else if (d._year == d1._year && d._month > d1._month)
{
return true;
}
else if (d._year == d1._year && d._month == d1._month && d._day > d1._day)
{
return true;
}
return false;
}
也很简单,但是要把类中的私有属性给换成公共的。
但是在调用函数的时候是 cout << Greater(d1, d) << endl;
这样调用的,
没有 d1 > d
这样看着直观。
为了增江代码的可读性,C++引入了运算符重载。运算符重载是指为自定义的数据类型,实现对已有运算符的重定义和扩展,使得程序能够像操作内置类型数据那样进行操作。
函数名:关键字operator后面跟需要重载的运算符符号
函数原型:返回值类型operator操作符(参数列表)
注:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this
.* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出
现
比如上面的代码,现在把它以运算符重载的方式实现
class Date
{
public:
Date(int year,int month,int day);
Date(const Date& other);
bool operator> (const Date& d1)
{
if (_year > d1._year)
{
return true;
}
else if (_year == d1._year && _month > d1._month)
{
return true;
}
else if (_year == d1._year && _month == d1._month && _day > d1._day)
{
return true;
}
return false;
}
private:
int _year;
int _day;
int _month;
};
Date::Date(const Date& other)
{
_year = other._year;
_month = other._month;
_day = other._day;
}
Date::Date(int year,int month, int day)
{
_year = year;
_day = day;
_month = month;
}
int main()
{
Date d(2023, 5, 9);
Date d1(2024, 5, 9);
cout << (d1 > d) << endl;
return 0;
}
这样,就可以直接使用>号来直接比较大小了。
同样的,日期+日期,减日期,都可以利用运算符重载来实现
当我们输出日期的时候,是调用的Date类中的Print函数,而自定义类型则可以使用cout,那么通过重载,自定义类型可以使用吗?
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
ostream& operator<< (ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
return out;
}
private:
int _year;
int _day;
int _month;
};
int main()
{
Date d(2023, 5, 13);
//d.Print();
d << cout;//出现这种情况是因为在调用这个重载的时候,是先传的this指针,d.operator<<(cout)跟这个形式等价
return 0;
}
上面的代码虽然也可以利用<<来实现输出,但是却是对象在前,那么怎么弄,可以把这个d << cout颠倒一下?
在类外重载就可以了。
在类外重载还面临一个问题----类外无法访问到类的私有属性,这个时候可以使用友元(指的是某个类或函数被允许访问另一个类中的私有成员)。
class Date
{
friend ostream& operator<< (ostream& out, const Date& other);
friend istream& operator>> (istream& in,Date& other);
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 _day;
int _month;
};
//<<中的other为什么带const而>>中的却不带,前者是输出,输出不需要改变原对象中的内容,故加const,而后者是输入,需要改变原对象的内容,所以不带const
ostream& operator<< (ostream& out,const Date& other)
{
out << other._year << "年" << other._month << "月" << other._day << "日" << endl;
return out;
}
istream& operator>> (istream& in, Date& other)
{
in >> other._year >> other._month >> other._day;
return in;
}
现在,就能对自定义类型的数据进行cin和cout操作了。
const成员
const成员是在类的定义中通过在函数声明后添加const关键字来指定的。const成员函数可以让我们确保该成员函数不会修改类的成员变量。
在类外部定义const成员函数时,需要在函数声明和函数定义中都加上const关键字。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _day;
int _month;
};
就比如这个,就可以在Print后面加上const,因为这个函数不会对对象做出任何改变。
再谈构造函数
构造函数体赋值
对象实例化的过程中,编译器会调用构造函数,来给成员变量一个值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
其实上面的代码,并不能被称之为初始化,称之为赋值更合适,因为初始化只能初始化一次,而赋值可以进行多次。
初始值列表
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
上面的代码中出现了新的东西,如冒号和花括号之间的代码。其中花括号定义了(空的)函数体。我们把新出现的部分叫做构造函数初始值列表,它负责新创建的对象的一个或者几个数据成员赋初值。构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来(或者在花括号内的)成员初始值。不同成员的初始化通过逗号分隔开来。
有时候我们可以忽略初始化和赋值的差异,但也有其他情况是我们不能忽略的,如果成员是引用类型或者const类型的话,我们就不能为其赋值,只能选择用初始值列表来实现。
class A
{
A(int a,int b)
:_a(a)
,_b(b)
{}
private:
const int _a;
int& _b;
};
注:
每个成员变量只能在初始化列表中出现一次
类中包含一下成员,必须放在初始化列表位置进行初始化
引用成员变量
const成员变量
自定义类型成员(且该类没有默认构造函数)尽量使用初始值列表,因为不管你是否使用初始值列表,对于自定义类型的成员变量,一定会先使用初始化列表
还有一点呢值得注意
成员在类中的声明次序就是其在初始化了i二标中的初始化顺序,与其在初始化列表中的先后顺序无关。
class Stack
{
public:
Stack(int capacity = 10)
:_capacity(capacity)
,_t(-1)
,_a((int*)malloc(sizeof(int)* _capacity))
{
if (_a == nullptr)
{
perror("malloc fail");
exit(-1);
}
}
private:
int* _a;
int _t;
int _capacity;
};
像上面的代码,初始值列表中先写的capacity,后是栈数组的初始化,但是初始化顺序不是这样的,是先给栈数组初始化,然后是_t,最后是 _capacity,所以这里给栈数组开空间中的操作是错误的。
explicit关键字
隐式类型转换相信大家都不陌生,那么自定义类型也可以隐式类型转换吗?
class A
{
public:
A(int x)
:_a(x)
{}
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A cc(2);
cc.Print();
A bb = 2;
bb.Print();
return 0;
}
这个代码会输出2 2
,
A cc(2)使用了显示构造函数进行对象实例化,但是A bb = 2,则使用了隐式类型转换,其实本质上是调用了构造函数A(int),将整型2转换成A类型的临时对象,然后借助临时对象调用拷贝构造函数将其赋值给了bb对象
如果不想让这个程序可以隐式类型转换,可以通过将构造函数声明为explicit加以阻止。
explicit关键字只对一个实参的构造函数有效,需要多个实参的构造函数不能用于隐式类型转换,所以无需将这些构造函数指定为explicit关键字。
只能在类内部声明构造函数的时候使用该关键字,在类外部定义时不应重复
static成员
在类中,是否存在一种成员,与对象无关,只与类有关呢?C++给出了答案:有的,声明为 static的类成员称类的静态成员,用static修饰的成员变量,称之为静态成员变量,用static修饰的成员函数,称之为静态成员函数
class A
{
public:
A(int a)
:_a(a)
{}
void ThisSum()
{
sum += _a;
}
static int GetSum();
private:
int _a;
static int sum;
};
int A::sum = 0;
int A::GetSum()
{
return A::sum;
}
int main()
{
A a(1);
A b(1);
b.ThisSum();
a.ThisSum();
cout << A::GetSum() << endl;
return 0;
}
上面的代码中会输出 2
,A类中声明了一个静态成员函数和一个静态成员变量(一般会在类中声明,在类外定义,定义的时候就不需要再重复写static了。通过类名::静态成员或者对象.静态成员可以访问到),然后进行了两次ThisSum的操作,可以看出,静态成员变量/函数都属于整个类,而不是单个对象,因此程序在运行的时候,对象创建了两次,而这连个静态的成员只创建一次。
注
静态成员函数没有this指针,不能访问任何非静态成员
静态成员也是类的成员,受public,private,provected访问限定符的限制
友元
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend)。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可。
class Time
{
friend class Date;
public:
Time(int a = 1)
:_time(a)
{}
private:
int _time;
};
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
void SetTime(int a)
{
_t._time = a;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date a(2023, 5, 14);
return 0;
}
上面的这个代码中,日期类是时间类的友元,所以在日期类中,可以访问时间类的成员。
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元的关系是单项的,不具有交换性(如上面的代码,可以在日期类中访问时间类的私有成员,但不可以在时间类中访问日期类的成员)
- 友元关系不能传递
- 友元关系不能继承
内部类
内部类,顾名思义就是定义在一个类中的类。内部类可以访问外部类的成员变量和成员函数,也就是说,内部类是外部类的友元。内部类可以看作是外部类的一部分,但是在外部类的成员函数或者其他外部类的代码中,不能直接使用内部类的名称, 需要使用作用域限定符。
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
class Time
{
public:
Time(int h,int m,int s)
:_hour(h)
,_minute(m)
,_second(s)
{}
private:
int _hour;
int _minute;
int _second;
};
private:
int _year;
int _month;
int _day;
};
int main()
{
cout << sizeof(Date) << endl;
return 0;
}
输出结果 12
,这是因为sizeof外部类=外部类,和内部类没有任何关系
cout << "外部类的大小" << ":" << sizeof(Date) << endl;
cout << "内部类的大小" << ":" << sizeof(Date::Time) << endl;
输出结果 12 12
通过作用域限定符可以访问到内部类。
注
内部类可以定义在外部类的public,protected,private都是可以的
内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
匿名对象
class Solve
{
public:
Solve(int a)
{
cout << "Solve" << endl;
}
~Solve()
{
cout << "~Solve" << endl;
}
};
int main()
{
Solve a(1);
Solve(1);
return 0;
}
在上面代码中,a是一个有名对象,a下面的是一个匿名对象(匿名对象指的是一个没有命名的临时对象),他跟有名对象有点不同,有名对象的生命周期在当前函数局部域,而匿名对象的声明周期在当行。
匿名对象是具有常性的,const引用延长匿名对象的生命周期,声明周期为当前函数局部域。
class Solve
{
public:
Solve()
{
cout << "Solve" << endl;
}
void Print()
{
cout << "----------------" << endl;
}
~Solve()
{
cout << "~Solve" << endl;
}
};
int main()
{
Solve().Print();
return 0;
}
拷贝对象时一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。
比如:一个表达式中,连续构造+拷贝构造,可以优化为一个构造
隐式类型中:连续构造+拷贝构造,优化为直接构造
连续拷贝构造+拷贝构造,优化为一个拷贝构造
一个表达式中,连续拷贝构造+赋值重载,无法优化
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a;
};
void f1(A aa)
{
;
}
A f2()
{
A aa;
return aa;
}
int main()
{
f1(1);//隐式类型
f1(A(1));//连续构造+拷贝构造
A a = f2();//连续拷贝构造+拷贝构造
}