1. 类
C++兼容C,C语言的struct的用法在C++还适用,但同时C++也将struct升级为类,类不仅可以定义变量,还可以定义函数。
1.1 类的定义
//C++习惯用class代替struct
class name
{
//类体:由成员变量和成员函数组成
};//和struct一样,都有分号
class为定义类的关键字,name是类的名字,{}内是类的主体。类的两种定义方式
- 声明和定义全部放在类体中。如果成员函数在类中定义,那么编译器可能将其当成内联函数。
class student
{
public://public和private是访问限定符,等下会讲到
void Print()
{
cout << _name << " " << _age << " " << _sex << endl;//_name和_age这些变量哪来的?等下会讲到
}
private:
char* _name;//为什么在成员变量前加_?这是成员变量的命名规则,还可以用mName,mAge,mSex来做变量名。
int _age;
char* _sex;
};
- 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::。
void student::Print()
{
cout << _name << " " << _age << " " << _sex << endl;
}
1.2 类的访问限定符
- 概念
访问限定符包括public(公有),private(私有),protected(保护)。它们是用于控制成员变量和成员函数的可见性和访问权限的关键字。
- 注意
(1)public修饰的成员在类外可以直接被访问。
(2)protected和private修饰的成员在类外不可以直接被访问,但在类内可以访问。
(3)访问权限作用域是从该访问限定符的位置开始直到下一个访问限定符出现为止。如果后面没有访问限定符,作用域就直接到类结束。
(4)class的默认访问权限为private,struct的默认访问权限为public(因为要兼容C)。
- 问题
C++中的struct和class的区别是多少?
C++的struct可以用来定义类,但它的默认访问权限为public,用class定义的类的默认访问权限是private。
- 封装
封装是将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。就像我们日常使用的电子设备,无需知道它们复杂的底层原理,只需知道它们提供的简单的按键。
那和类的访问权限有什么关系?
在C++中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
1.3 类的作用域
在类外如何定义类的成员?类定义了一个新的作用域,类的成员在这个作用域。所以要访问类的成员就得用类作用域访问限定符(::)。
class Date//日期类
{
public:
void Print();
private:
int _year;
int _month;
int _day;
};
void Date::Print()
{
cout << _year << "-" << _month << "-" << _day;
}
int main()
{
Date d1;
d1.Print();//类访问成员的方式和结构体一样
}
1.4 类的实例化
- 概念
用类创建对象的过程,就是类的实例化。 - 类就像一张蓝图,实例化就像依照这张蓝图建立起的建筑物。只有实例化之后,才有实际空间,类本身是没有空间的。
class Date//日期类
{
public:
void Print();
private:
int _year;
int _month;
int _day;
};
void Date::Print()
{
cout << _year << "-" << _month << "-" << _day;
}
int main()
{
Date d1;
d1.Print();//正确的。实例化后有实际的空间
Date.Print();//错误的。没有实例化。
return 0;
}
1.5 类的大小
class Date//日期类
{
public:
void Print()
{
cout<<_year<<"-"<<_month<<"-"<<_day;
}
private:
int _year;
int _month;
int _day;
};
- 问题
问题1
sizeof(Date)==?答案是12。问题2
怎么计算的?只算成员变量,不算成员函数。问题3
对象的空间只存储成员变量,没有存储成员函数?如果存储了成员函数,当一个类创建多个对象时,每个对象都会存储相同的成员函数,浪费空间。问题4
这些成员函数存储在哪?存储在公共代码区,里面有类成员函数表,当需要的时候就去公共代码区调用。总结
一个类的大小,就是成员变量的大小。
- 类的大小的计算
类同样遵循结构体内存对齐规则。
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数(min = {该成员变量的大小,编译器默认的对齐数})的整数倍的地址处。
- 结构体的总大小 = 这些成员变量的最大对齐数的整数倍。
- 如果出现嵌套结构体,嵌套结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
- 特殊情况
//类中仅有成员函数
class A1
{
public:
void f1() {};
};
//空类
class A2
{};
int main()
{
//计算它们的大小
int size1 = sizeof(A1);
int size2 = sizeof(A2);
cout << size1 << endl;
cout << size2 << endl;
A1 a1;
A2 a2;
//如何证明这两变量有空间
cout << &a1 << endl;
cout << &a2 << endl;
return 0;
}
打印结果
没有成员变量的类对象,大小为1,是因为需要1字节占位,表示对象存在。
空类比较特殊,大小为1,编译器给了空类一个字节来唯一标识这个类的对象。
2. this指针
2.1 例子
class Date//日期类
{
public:
void DateInit(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day;
cout << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.DateInit(2023, 5, 16);
Date d2;
d2.DateInit(2023, 6, 15);
d1.Print();
d2.Print();
}
我们发现我们初始化和打印的时候,并没有传参数给两个函数,两个类又调用的是同一函数,那么函数又是如何区分d1和d2的?里面的_year和_month这些变量又是从何而来?
这是因为这些成员函数都配备有一个隐藏的this指针,这个指针指向调用该函数的对象。在函数中所有对成员变量的操作都是通过这个this指针来完成,只不过我们看不到而已。
void Print()
{
cout << _year << "-" << _month << "-" << _day;
cout << endl;
}
//等价于
void Print()
{
cout<<this->_year<<"-"<<this->_month<<"-"<<this->_day;
cout<<endl;
}
2.2 this指针的特性
- this指针不能在实参和形参中显示传递。
- this指针的类型是类类型* const,(例如Date * const this),this的内容不能改变。
- this指针是实际存在,是函数的第一个参数,本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参,所以对象中不存储this指针(调用方不存储this指针)。
A::Print();//所以不能用类去访问调用成员函数,因为this指针不明确,this必须是对象的地址。
- this指针是“成员函数”的第一个隐含的指针形参,一般情况下由编译器通过ecx寄存器自动传参,不需要用户传递。
- this指针是函数的参数,存储在函数调用的栈帧。
2.3 练习
//这段代码会不会报错?
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
并不会。p是空指针,调用Print()。但Print()存储在公共区域,不会发生越界访问。p作为函数参数,却没有被调用,也不会报错。所以这段代码是可以正常运行的。
//这段代码会不会报错
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
会。p调用的Print()中,p访问_a,造成非法访问。
在一个类中,如果我们什么都没有,编译器就会默认生成六个默认成员函数。默认成员函数就是用户没有显示实现,编译器会生成的成员函数。接下来,就逐一介绍。
3. 构造函数
- 概念
构造函数负责解决类的初始化,它与类同名。在创建对象时编译器会自动调用,保证每个成员变量都有初始化。
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;
int _month;
int _day;
};
问题
这不是一个初始化函数就能解决的问题吗?为什么还特地让编译器调用函数来解决?
有时候初始化很麻烦,有时候忘记实现初始化,有时候写了又忘记调用。还不如在定义对象的同时就让编译器自动调用,省去很多麻烦。
- 特性
(1)不要被表象迷惑,构造函数并没有创造的作用,它是负责初始化对象的。
(2)构造函数没有返回值,也不需要写void。
(3)构造函数与类同名。
(4)编译器会自动调用构造函数(不管我们有没有显示实现)。
(5)构造函数可以重载(这意味着我们有多种初始化的方式)。
//带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//无参构造函数
Date()
{
;
}
//这两个构造函数构成函数重载。
那么如何调用构造函数呢?带参的构造函数需要我们传参,无参的构造函数不需要传参,该怎么传参。
Date d1(2023,5,20);//调用带参构造函数
Date d2;//调用无参构造函数,因为没有传参。
//对象后为什么不带上()?Date d2()变成了函数声明。编译器不好识别。Date表示返回值类型,d2是函数名,()括号内是参数列表。所以不带()。
(6)编译器自动生成的默认构造函数是无参的,如果我们自己显示实现构造函数,编译器就不会生成。
(7)不显示实现构造函数,编译器会自动生成,内置类型不做处理,自定义类型调用其默认构造函数。(内置类型/基本类型:语言本身定义的类型int/char/double/指针类型;自定义类型:用struct/class定义的类型)
a. 所以,在一般情况下,有内置类型的成员就要自己实现构造函数,不能用编译器默认生成的默认构造函数(因为没有对内置类型初始化)。
b. 内置类型成员都有缺省值,且初始化符合我们要求,或者全是成员全是自定义类型,这时就可以使用编译器自动生成的默认构造函数。
(8)自定义类型调用其默认构造,如此不断调用下去,最终的自定义类型的成员都是内置类型。
(9)无参构造函数(我们自己写的、编译器自动生成的)和全缺省构造函数都叫做默认构造函数。但不能同时实现,因为默认构造函数只能有一个。
Date()
{
_year = 2000;
_month = 1;
_day = 1;
}
Date(int year = 2000,int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int main()
{
Date d1;
}
这段代码存在调用歧义?一个是无参构造函数,一个是全缺省构造函数,d1调用哪个都可以,编译器不知道调哪个,存在调用歧义。
4. 析构函数
- 概念
析构函数负责清理对象中的资源。对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
- 特性
(1)析构函数并不是销毁对象,而是在对象销毁时编译器自动调用的用来清理对象内资源的函数。
(2)析构函数没有参数,也没有返回值。所以析构函数没有重载。
(3)析构函数的函数名就是在类名前加上~。
(4)对象销毁时,编译器会自动调用析构函数。
(5)如果没有显示实现,编译器会自动生成默认析构函数。
(6)编译器自动生成的析构函数,对内置类型不做处理,对自定义类型会调用它的析构函数。
a. 在一般情况下,有动态申请资源,就需要显示写析构函数释放资源。
b. 没有动态申请资源,不需要写析构函数,在栈上开辟的空间,在程序结束后自动释放。
- 例子
class Stack
{
public:
Stack(size_t capacity = 3)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (NULL == _a)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_num = 0;
}
~Stack()
{
free(_a);
}
private:
int* _a;
int _num;
int _capacity;
};
class A
{
public:
private:
Stack _s;
int _num = 1;
};
A类不用写构造函数,因为内置类型有缺省值,自定义类型会调用其构造函数;A也不用写析构函数,因为内存类型不用考虑,程序结束后自动销毁,自定义类型会调用其析构函数。Stack就必须写构造函数,因为没有缺省值 ;也必须写析构函数,因为有动态申请的内存需要释放。
5. 拷贝构造函数
- 概念
拷贝构造是将一个对象的内容拷贝给另一个对象。在用已存在的类类型对象创建新对象时由编译器自动调用。
- 特性
(1)拷贝构造是构造函数的重载。参数是要拷贝的同类的对象。
(2)拷贝构造函数的参数只有一个且必须是是类类型的引用,使用传值方式编译器会直接报错,因为会引发无穷递归调用。
class Date
{
public:
//构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date d)//错误的
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year ;
int _month ;
int _day ;
};
int main()
{
Date d1(2023,5,20);
Date d2(d1);
return 0 ;
}
所以拷贝构造函数的正确写法是
Date(const Date& d)//为什么加const?缩小权限
{
_year = d._year;
_month = d._month;
_day = d._day;
}
(3)如果我们没有显示定义拷贝构造,编译器会自动生成拷贝构造函数,默认生成的拷贝构造函数对象按内存存储按字节完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
a. 内置类型完成值拷贝/浅拷贝。
b. 自定义类型会调用其拷贝构造。
(4)既然编译器默认生成的拷贝构造能进行值拷贝,还需要自己显示实现吗?肯定要的。如果单纯依靠编译器的默认拷贝构造函数,下面的代码就会出错。
class Stack
{
public:
Stack(size_t capacity = 3)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (NULL == _a)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_num = 0;
}
~Stack()
{
free(_a);
}
private:
int* _a;
int _num;
int _capacity;
};
int main()
{
Stack s1(10);//开辟一个容量为10的栈
Stack s2(s1);//将s1拷贝给s2
return 0;
}
缺点
a. Stack没有显示实现拷贝构造函数,依靠编译器默认生成的拷贝构造函数,将s1的内容直接拷贝给s2。此时s1和s2同时指向同一片空间。当s1和s2的生命周期要结束时,s1和s2要被销毁,此时先销毁s2,编译器会调用s2的析构函数,释放s2中动态申请的资源。可是s1同时也指向释放掉的资源,再释放一次,就会造成程序崩溃。
b. s1和s2指向同一块空间,一个修改,另一个也会修改。改进
那该如何写拷贝构造函数?以栈为例子。
//拷贝构造函数
Stack(const Stack& s)
{
//得自己开辟空间
_a = (int*)malloc(sizeof(int) * s._capacity);
if (NULL == _a)
{
perror("malloc fail");
}
//将内容拷贝过来
memcpy(_a, s._a, sizeof(int) * s._num);
_num = s._num;
_capacity = s._capacity;
}
(5)应用场合
a. 使用已存在对象创建新对象
b. 函数参数类型为类类型对象
c. 函数返回值类型为类类型对象
6. 赋值运算符重载
6.1 运算符重载
- 例子
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,5,20);
Date d2;
//如果我们想要将d1的内容赋值给d2,得利用拷贝构造,
//但是拷贝构造必须在定义的时候进行初始化,而d2已经定义了
//此时该如何将d1的内容赋值给d2。
}
需要写一个类成员函数,因为Date类的成员变量是私有的,不能在类外被访问;或者直接将成员变量变为公有的。但这样太麻烦了,有更方便的方法吗?像变量之间的简单赋值一样,能不能写成d2 = d1。可以,需要运算符重载。
- 概念
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。函数名为operator + 需要重载的运算符符号,函数原型为:返回值类型+operator+运算符(参数列表)。
//改进
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//赋值运算符重载
const Date& operator=(const Date&d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,5,20);
Date d2;
d2 = d1;//可以直接赋值
return 0;
}
这里面的一些细节等下再讲,现在只需知道类类型之间可以直接进行赋值。
- 注意
(1)不能通过连接其他符号来创建新的操作符,比如oerator@。
(2)重载操作符必须有一个类类型参数。这个运算符有多少操作数,运算符重载就有多少参数。
(3)用于内置类型的运算符,其含义不能改变,例如:内置的整形+,不能改变其含义。
(4)作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this;若不作为类成员函数重载,则要考虑成员变量的访问权限。
//以重载==运算符为例
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//赋值运算符重载
const Date& operator=(const Date&d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//private:
int _year;
int _month;
int _day;
};
//全局的operator
bool operator==(const Date& d1,const Date& d2)
{
//需要访问到Date中的成员变量,要么将成员变量权限修改为public,
//如果还要考虑到类成员变量的封装性,就将运算符重载定义在类内。
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
int main()
{
Date d1(2023,5,20);
Date d2(2000,1,1);
cout<<(d1==d2)<<endl;//<<和>>优先级高于==,所以就加()。
}
(5)这五个运算符不能重载:*.
( * 和.是一体的),::
,sizeof
,?:
,.
。
6.2 赋值运算符重载
这是编译器默认生成的第4个默认成员函数。
- 特性
(1)函数形式:const T& operator=(const T& parameter)。参数类型和返回值类型为const T&,不需要调用其拷贝构造,提高效率。问题
为什么要有返回类型?d1 = d2 = d3。将d3赋值给d2,返回d2,再将d2赋值给d1。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
赋值运算符重载
const Date& operator=(const Date&d)
{
//如果是自己给自己赋值就不执行操作
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
}
//private:
int _year;
int _month;
int _day;
};
(2)赋值运算符只能重载成类的成员函数,不能重载成全局函数。
为什么?因为赋值运算符是默认成员函数。如果我们将赋值运算符重载成全局函数,编译器就会当我们没有显示实现赋值运算符重载,会默认生成赋值运算符重载。当我们在调用它时,编译器就会报错,因为全局的赋值运算符重载和编译器默认生成的赋值运算符重载冲突。
(3)如果我们没有显示实现,编译器会默生成赋值运算符重载,以值的方式逐字节拷贝。
a. 内置类型成员是直接赋值;
b. 自定义类型成员会调用其赋值运算符重载。注意
这点和拷贝构造函数相同
(4)如果类中成员动态申请资源,那么我们必须自己实现赋值运算符重载,否则会出现于拷贝构造函数相同的问题。
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (int*)malloc(capacity * sizeof(int));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const int& data)
{
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
int* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1(10);
Stack s2(20);
s2 = s1;
}
(5)赋值运算符重载和拷贝构造函数的区别
int main()
{
Date d1(2023,5,21);
Date d2(2023,5,22);
d1 = d2;//(1)已存在的两个对象之间的复制拷贝,本质是运算符重载
Date d3(d1);//用一个已经存在的对象初始化另一个对象,本质是拷贝构造
//(2)赋值操作符有返回值,返回左边的操作数
Date d3 = d2;//(3)用一个已经存在的对象去初始化另一个对象,这是拷贝构造
//相当于Date d3(d2);
return 0;
}
6.3 完整的日期类的实现
class Date
{
public:
//构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//注意:是否要重载运算符取决于该运算符对这个类是否有意义
//拷贝构造函数
Date(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
}
//==运算符重载
bool operator==(const Date& d)
{
return (_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;
}
//<运算符重载
bool operator<(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;
}
return false;
}
//<=运算符重载
bool operator<=(const Date& d)
{
return *this < d || *this == d;
}
//>运算符重载
bool operator>(const Date& d)
{
return !(*this <= d);
}
//>=运算符重载
bool operator>=(const Date& d)
{
return !(*this < d);
}
//!=运算符重载
bool operator!=(const Date& d)
{
return !(*this == d);
}
//+=运算符重载
int GetMonthDay(int year, int month)
{
int days[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if ((month == 2) && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
days[2] = 29;
}
return days[month];
}
Date& operator+=(int day)//days是要增加的天数
{
//这是考虑到day为负数的情况,可以学完后面的-=运算符重载再来看
if (day < 0)
{
return *this -= (-day);
}
//要得到增加后的日期,直接加到_day,让_day减去当前月的天数,
//同时让_month+1,直到_day小于等于当前月的天数
_day += day;
while(_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
if (_month == 12)
{
_year++;
_month = 1;
}
else
{
_month++;
}
}
return *this;
}
//+运算符重载
//注意:不能返回引用,因为tmp是局部变量,出了函数就销毁
Date operator+(int day)
{
Date tmp(*this);//拷贝构造一个临时变量
tmp += day;
return tmp;
}
//++运算符重载
//前置++:返回+1之后的值
Date& operator++()
{
*this += 1;
return *this;
}
//后置++:返回+1之前的值
//为了与前置++构成重载,C++规定在后置++重载时增加一个int类型的参数
Date operator++(int)//不写形参,因为写了没意义,仅仅为了占位,与前置++构成重载,方便区分
{
Date tmp(*this);
*this += 1;
return tmp;
}
//-=运算符重载
Date& operator-=(int day)
{
if (day < 0)
{
return *this += (-day);
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
//-运算符重载
Date operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
//--运算符重载
//与++运算符重载类似
//前置--
Date& operator--()
{
*this -= 1;
return *this;
}
//后置--
Date operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
//算出两个日期之间的天数:d1-d2
int operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;//标志返回值的正负
if (*this < d)
{
max = d;
min = *this;
//如果第一个参数小于第二个参数,将flag置为-1
flag = -1;
}
//利用循环,直接算出两日期之间的天数
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
private:
int _year;
int _month;
int _day;
};
- <<运算符重载
问题1
如果我们想要直接打印Date类的对象,如cout<<d,我们需要对<<进行运算符重载。问题2
前面不是说cout能自动识别变量类型,进行打印吗?为什么还需要自己显示实现?可以支持内置类型,因为C++库里面已经实现。自定义类型的打印需要自己显示实现。代码
void operator<<(ostream& out)
{
cout<<_year<<"年"<<_month<<"月"<<_day<<"日";
}
但我们发现调用这个成员函数时,如果以cout<<d1会报错,因为实参传递顺序反了,必须以d1<<cout这样实现才可以顺序通过。但格式看起来很奇怪,意思好像是cout流向d1,该怎么解决?改进1
void operator<<(ostream& out,const Date& d)
{
cout<<d. _year<<"年"<<d. _month<<"月"<<d. _day<<"日";
}
流插入不能写成成员函数。Date对象默认占用第一个参数,就做了左操作数,写出来一定是d1<<cout,所以写到全局。
但又面临私有成员的访问问题,此时就要用到友元函数(后面会学到)。改进2
class Date
{
//声明我是你(Date)的朋友(在类内哪里声明都可以)
friend void operator<<(ostream& out,const Date& d);
………
}
声明完后,我们就可以使用类的私有成员。
现在,又有问题,如果我们想要连续打印,该如何修改?例如cout<<d1<<d2。改进3
ostream& operator<<(ostream& out,const Date& d)
{
cout<<d. _year<<"年"<<d. _month<<"月"<<d. _day<<"日";
return out;
}
-
>>
运算符重载
istream& operator>>(istream& in, Date& d)//d不用const修饰,是因为d的内容要改变
{
cin >> d._year >> d._month >> d._day;
return in;
}
7. 取地址以及const取地址操作符重载
这两个函数如果不显示实现,编译器会默认生成,是最后的两个默认成员函数。但一般我们不用自己实现,利用编译器自动生成的就够了。
class Date
{
public :
Date* operator&()//这两个函数构成函数重载
{
return this ;
}
const Date* operator&()const//这个const修饰的是this指针指向的内容,接下来会讲到
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
8. const修饰this指针
const修饰的成员函数叫做const成员函数,实际修饰的是this指针。
- 问题
这段代码为什么会报错?
- 解决
此时需要缩小this的权限,但该如何缩小,它又不出现在参数列表,所以直接在()后面加上const
void Print() const
{
cout << _year << "年" << _month << "月" << _day << "日";
}
成员函数后面+const,普通对象和const对象都可以调用成员函数。问题
是否所有的成员函数都要加const?
不是,要修改的对象成员变量的函数不能加。例如:operator+=,operator-=。
- 结论
const在成员函数的声明和定义都得加,只要成员函数内部不修改成员变量,都应该加const,这样const对象和普通对象都已调用。
9. 成员变量的初始化
成员变量的初始化不是已经讲过吗?在构造函数内部进行赋值,那并不是初始化,而是对成员变量的赋初值。因为初始化只能初始化一次,而构造函数内可以多次赋值。
- 初始化列表
初始化列表就是用来初始化成员变量的。它以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式。
构造函数有两种赋值方式
//初始化列表
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;
};
- 问题
为什么会有初始化列表?
有些成员变量赋值不能通过构造函数体赋值来实现,只能通过初始化列表来实现。
以下三种成员变量必须放在初始化列表位置进行初始化:
(1)引用成员变量
(2)const成员变量
(3)自定义类型成员(且该类没有默认构造函数)
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int& ref)
:_aobj(a)
, _ref(ref)
, _n(10)
{}
private:
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
注意
(1)每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次)。
(2)缺省值是给初始化列表准备的。但如果初始化列表显示赋值,就不用缺省值。
(3)如果自定义类型无默认构造函数,我们必须显示给其赋值,否则不能编译通过;自定义类型有默认构造,可以不显示调用,也可以显示调用。
(4)尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
(5)成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无光关。所以声明顺序和定义顺序保持一致。
class A
{
public:
A(int a)
:_a1(a)//后执行
,_a2(_a1)//先执行
{}
void Print()
{
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
}
所以最终_a2是随机值,_a1是1。
(6)既然初始化列表这么厉害,为什么还要函数体赋值?
因为有些工作在初始化列表不能完成。
//例如:动态开辟二维数组(先开一维指针数组,再用每个指针开辟数组)
class AA
{
public:
AA(int row,int col)
:_row(row)
,_col(col)
{
_a = (int**)malloc(sizeof(int*)*row);
for(int i = 0;i < row;i++)
{
_a[i] = (int*)malloc(sizof(int)*col);
}
}
private:
int _row;
int _col;
int _a;
}
10. explicit关键字
- 例子
class A
{
public:
//构造
A(int a)
:_a(a)
{
cout << "A(int a)" << endl;
}
//拷贝构造
A(const A& d)
{
_a = d._a;
cout << "A(const A& d)" << endl;
}
private:
int _a;
};
int main()
{
A aa1(1);//调用构造函数
A aa2 = 2;//2不能直接赋值给aa2,涉及隐式类型转换
//先用2构造一个A的临时变量(A tmp(2)),再拷贝构
//造aa2,本来会调用拷贝构造函数和构造函数的,但编
//译器会直接优化,用2直接构造
return 0;
}
什么情况下编译器会优化?在同一表达式,连续的构造一般都会优化。
A& aa3 = 2;//(×)
const A& aa4 = 2;//(gou)(√)
//隐式类型转换会生成临时变量,临时变量具有常性,只能用const修饰。
但如果不想让转换发生,那就+explicit。
- 定义
从上面的例子,不难看出构造函数据具有类型转换的功能,在构造函数前+explicit修饰,禁止类型转换。
- 总结
用explicit修饰构造函数,将会禁止构造函数的隐式类型转换。
11. static成员
- 用static修饰的成员变量叫做静态成员变量,用static修饰的成员函数叫做静态成员函数。
//例子:统计现在有多少个类对象
class A
{
public:
A() { ++_scount; }
A(const A& t) { ++_scount; }
~A() { --_scount; }
static int GetACount() { return _scount; }
private:
static int _scount;
};
int A:: _scount = 0;//静态成员变量一定要在类外进行初始化。为什么?等下讲。
void Func1()
{
A aa;
cout<<A::GetACount()<<endl;//2个。但aa出了作用域会销毁。
}
void Func2()
{
static A aa;
cout<<A::GetACount()<<endl;//3个。
}
int main()
{
cout<<A::GetACount()<<endl;//0个。为什么用类调用静态成员函数?等下讲。
A aa1;//1个。调用构造。
Func1();
A aa2(aa1);//2个。调用拷贝构造。
Func2();
return 0;
}
- 特点
(1)全局变量也可以代替_scount的工作,但是在任何地方都可以随意修改,所以用有访问限制的静态成员变量来代替。
(2)静态成员为所有类对象共享,不属于某个具体的对象,存放在静态区。
a. 成员变量属于每个类对象,存储在对象里面,静态成员变量,属于类的每个成员共享,存储在静态区。所以不能再类中对其进行初始化,否则每个类对象都要对其进行初始化,这不就乱套了。
b. 静态成员函数没有this指针,指定域和访问限定符就可以访问。但它不能访问成员,因为没有this指针。
(3)静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明。
(4)静态成员也是类的成员,受public、protected、private访问限定符的限制。
int A::_scount = 0;
//_scount不是私有成员吗?为什么能在类外访问?这是特殊情况(初始化),在其余情况下都不能访问。
(5)静态成员可用类名::
静态成员或者对象.
静态成员来访问。例如A::
_scount。
(6)静态成员函数可以调用静态成员变量,静态成员函数不可以调用非静态成员函数(没有this指针),静态成员函数可以调用类的静态成员函数(类内不受限制)。
12. 友元函数和友元类
为了解决类外访问权限的问题,友元出现了。
- 友元函数
在前面日期类的实现中,我们从流插入和流提取中已经了解到,友元函数可以直接访问类的私有成员,需要在类内声明,同时+friend,类外定义。
但还有几点要注意:
(1)友元函数在类内声明,并不代表其是类的成员函数。这也就意味着不能在函数后面+const,因为没有this指针。
(2)友元函数不受访问限定符限制,可以在类中任何地方声明。
- 友元类
(1)一个函数可以是一个类的友元,那一个类也可以是一个类的友元。
class A
{
friend class B;
public:
void Print()
{
cout << _a << " " << _b << endl;
}
private:
int _a = 10;
double _b = 3.14;
};
class B
{
public:
void Print()
{
cout << aa._a << " " << aa._b << endl;
}
private:
int _i = 1;
A aa;
};
int main()
{
B bb;
bb.Print();
return 0;
}
(2)友元关系是单向的,不具有交换性。
比如B是A的友元,B内有A类型的成员,B可以通过它访问A的非公有成员;而A不能访问B的非公有成员。
(3)友元关系不能传递
如果B是A的友元,C是B的友元,不能说明C是A的友元。
13. 内部类
- 概念
在一个类中定义另一个类,这个定义的类叫做内部类。内部类就是外部类的友元,内部类可以通过外部类的对象参数来访问外部类中的所有成员,但外部类不是内部类的友元,不能通过外部类的对象去访问内部类的成员。内部类是一个独立的类,不属于外部类。
- 计算一个类的大小
class A
{
private:
static int k;//不需要算,因为k不在对象里面
int h;
public:
class B // B天生就是A的友元 //也不需要算,因为A中没有用B定义对象
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
所以sizeof(A) == 4。
- 如果B是私有的,也会收到访问限定符的约束。因此,内部类有公有的和私有的。
14. 匿名对象
- 匿名对象就是没有名字的对象。
A aa1(1);//有名对象
A(2);//匿名对象
- 没有名字,怎么使用?匿名又有什么用?
class A
{
public:
void Print()
{
cout << "_a的值为:" << _a << endl;
}
A(int a)
:_a(a)
{
cout << "A()" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A aa1(1);
A(2).Print();
return 0;
}
直接使用。当你只需要类中某一个函数时,你又不想特地去创建一个变量,这时就可以使用匿名对象,因为它即用即销毁,不占用空间,提高效率。
- 匿名对象即用即销毁,生命周期在当前行;有名对象生命周期在当前函数局部域。
- 匿名对象具有常性。
A& aa = A(1);//(×)
const A& aa = A(1);//(√)
同时const引用延长了匿名对象的生命周期,变成了当前函数的局部域。因为要考虑到aa可能用到,所以先不销毁。
15. 优化构造函数
例子
class A
{
public:
void Print()
{
cout << "_a的值为:" << _a << endl;
}
A(int a)
:_a(a)
{
cout << "A()" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
- 补充下:const修饰和没const修饰是两种类型,构成重载函数
void Func1(A aa)
{
;
}
void Func1(const A& aa)
{
;
}
int main()
{
A aa1(1);
Func1(aa1);
return 0;
}
Func1(aa1)存在调用歧义,不知道调用哪个?第一个是拷贝构造,第二个是缩小权限,都可以被调用。
- 引例
A Func2()
{
A aa(2);
return aa;
}
int main()
{
Func2();
return 0;
}
调用情况
文章来源:https://www.toymoban.com/news/detail-459145.html
A Func2()
{
A aa;
return aa;
}
int main()
{
A& aa = Func2();//(×)
const A& a = Func2();//(√)//权限的平移
return 0;
}
Func2返回aa,调用一次拷贝构造,再将其赋值给a,调用一次拷贝构造,但编译器是一次拷贝,原因是编译器在连续的构造会优化,在同一行同一个表达式中连续的构造和拷贝构造会优化为一次拷贝。文章来源地址https://www.toymoban.com/news/detail-459145.html
A aa1(1);
Func1(aa1);/不会优化
Func1(A(1));//会优化,构造+拷贝构造 -》构造
A aa2 = 1;//隐式类型转化,会优化,构造+拷贝构造-》构造
Func1(1);//会优化,类似于A aa = 1
到了这里,关于【C++】类和对象的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!