类和对象
一.类和对象基本概念
1.面向过程和面向对象的区别
面向过程就是分析出解决问题的步骤,用函数将这些步骤实现,使用的时候依次调用即可;
面向对象就是将构成问题的事务分解成各个对象,每个对象描述某个事物在整个问题中的行为,使用的时候定义对象,通过对象方法的调用解决问题;
面向过程的优点:性能比面向对象高,因为类调用时需要实例化,比较消耗资源
面向过程的缺点:没有面向对象容易维护、扩展和复用
面向对象的优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态的特性,继承可以让对象更加庞大,实现代码复用,多态可以让操作更加灵活;
面向对象的缺点:性能比面向过程低
2.类的引入和定义
1)类的引入
在C++中, 结构体内部不仅可以定义变量,也可以定义函数,这样的结构体称之为类
2)类的定义
通过关键字class/struct定义类,不同的是class定义的类内部成员都是私有的,而用struct定义的内部成员是共有的
类定义的格式:
关键字 类名
{
类内部成员或函数
}; //末尾的这个分号不能丢,这是格式要求
示例:
class Base
{
int property;
void Method()
{}
};
类定义的两种方式
声明和定义全部放在类体中;
声明和定义分离,声明放在.h文件中,定义放在.cpp文件中;
一般建议采用声明和定义相分离;
3.类的访问限定符和封装
1)访问限定符
通过访问限定符可以为类中的成员和方法设置访问权限
访问限定符有三种,按照优先级从高到低为:
private、protected、public
访问限定符简介
private:
用private修饰的成员只能在类内部调用,类的外部不能调用
protected:
用protected修饰的成员可以在类的内部调用,也可以被派生类调用,但不能在类的外部调用
public:
用public修饰的成员在类的内部和外部都可以调用
访问限定符的作用范围
上一个访问限定符到下一个访问限定符之间,如果没有下一个访问限定符,就从该访问限定符出现的位置到类的尾部
2)封装
将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。通过对类内部的成员的访问权限的限制,将类可以抽象为一个整体,这就是封装
4.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域
示例:
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout<<_name<<" "_gender<<" "<<_age<<endl; }
对比: 类是不能跨源文件的,而命名空间可以跨源文件
5.类的实例化
类只是一个模型,定义了遵守这个模型规则的事物的结构,它没有实体,需要通过定义该类型的变量来搭建出这个模型对应的实物, 这就是实例化
6.类的存储模型
类中只存储类的成员变量,而类的成员函数都存放在代码段,当然的,类的成员变量需要进行内存对齐
空类比较特殊,编译器给了空类一个字节来唯一标识这个类
7.this指针
1)this指针的引入
当一个类中有多个成员函数并且这些成员函数的函数体中没有关于不同对象的区分,编译器如何知道是哪个对象在调用对应的成员函数呢?
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
2)this指针的特性
a、this指针的类型:类类型* const;
b、只能在“成员函数”的内部使用;
c、this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针;
d、this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递;
e、只有类内部的非静态成员才有this指针;
f、this指针的指向不能发生改变,this指针是一个右值;
测试如下:
g、this指针可以为空,但是不能进行解引用;
测试代码如下:
class Date
{
public:
Date(int year = 2021, int month = 10, int day = 8)
:_year(year)
, _month(month)
, _day(day)
{}
void showDate()
{
cout << _year << " 年 " << _month << " 月 " << _day << " 日" << endl;
}
void NullThisTest()
{
cout << "NullThisTest" << endl;
}
private:
int _year; //年
int _month; //月
int _day; //日
};
int main()
{
Date* date = nullptr;
date->NullThisTest();
date->showDate();
return 0;
}
测试结果:
当不需要对this指针进行解引用时,this指针可以为空,但是当需要对this指针进行解引用时,
this指针为空就会报错
3)this指针的存放位置
this指针的存储位置:寄存器
this指针是一个函数的形参,所以一般会在栈区,但因为this指针使用的频率比较高,为了优化提高效率,所以编译器将其放到了寄存器中
4)this指针的作用
解决名称冲突;
返回对象本身;
二.类的默认成员函数
一个空类至少有6个默认成员函数,对于自定义的类型,如果不定义这些成员函数,编译器会帮助我们生成
类的6个默认成员函数:
(1)构造函数:初始化资源
(2)析构函数:清理资源
(3)拷贝构造:使用同类对象初始化创建对象
(4)赋值重载:把一个对象赋值给另一个对象
(5)对普通对象取地址重载: 对普通对象取地址
(6)对const对象取地址重载: 对const对象取地址
1.构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。在构造函数调用之前,成员就已经存在了,构造函数的作用仅仅是初始化成员,而不是生成类的成员。
构造函数支持函数重载,也就是一个类中可以有多个构造函数,满足不同场景的需要
1)构造函数的形式
类名(参数列表)
{函数体}
2)编译器生成的默认构造有什么作用?
如果一个类中有自定义成员,编译器生成的默认构造会自动调用自定义成员的默认构造先完成自定义成员的初始化。
3)默认构造函数
无参构造函数和全缺省的构造函数都可以作为默认构造函数,但是默认构造只能有一个
4)构造函数的特点
(1)构造函数负责初始化对象类,它的函数名和类名相同;
(2)编译器在创建对象时,会自动调用默认构造函数,完成对象内容的初始化;
(3)可以自定义带参的构造函数
类名(参数列表)
{
函数体
}
示例
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
(5)构造函数可以定义多个,支持函数重载;
(6)一个类只能有一个默认构造,一般使用全缺省构造,全缺省构造和无参构造只能存在一个;
反面示例:
(7)当前类的构造函数会首先自动调用自定义成员的默认构造,完成自定义成员的初始化,然后调用自身基本类型(内置类型)的默认构造。如果自定义类型没有默认构造,编译器就会报错;
(8) 没有显示定义任何一个构造函数,编译器会自动生成无参构造;
(9)构造函数不能声明为虚函数;
构造函数不能声明为虚函数的具体介绍见下面大佬的博客:
链接
(10)构造函数不能显示调用;
Date d;
//这是错误的调用方式
d.Date(1,2,3);
(11)无参构造函数调用时,对象名后面为空;
举个例子:
Date date; 是正确的,
而不是 Date date(); ,这是定义了一个参数列表为空、返回值为Date对象的函数
5)构造函数的初始化
a.初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
初始化列表使用的注意事项:
I. 每个成员变量在初始化列表中只能出现一次;
II. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关;
III. 对于自定义类型成员变量,一定会先使用自定义类型的初始化列表初始化;
IV. 类中包含以下成员,必须放在初始化列表位置进行初始化:
引用成员变量,const成员变量,没有默认构造函数的自定义类型成员
b.给声明的变量一个缺省值
C++11允许在声明变量时,给变量赋予一个缺省值,在变量没有被初始化时,它的值就是这个缺省值,但缺省值的调用顺序在初始化列表之后
6)单个参数构造的隐式类型转换
当构造函数只有一个参数时,可以用一个数值构造一个匿名对象,将这个匿名对象转换为该类型对象,对于这种隐式类型转换,可以通过explicit关键字禁止编译器进行单个参数的构造函数的隐式类型转换
没有禁止单个参数构造函数的隐式类型转换:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year)
:_year(year)
{}
private:
int _year;
};
int main()
{
Date d = 1024;
return 0;
}
程序启动之后编译器借助1024生成了一个无名对象,将这个无名对象赋值给d
加了explicit关键字之后,禁止了隐式类型转换:
#include <iostream>
using namespace std;
class Date
{
public:
explicit Date(int year)
:_year(year)
{}
private:
int _year;
};
int main()
{
Date d = 1024;
return 0;
}
运行报错:
2.析构函数
析构函数是特殊的成员函数,在对象销毁时会自动调用用来进行资源的清理,比如堆上开辟的空间的释放或者文件描述符的关闭,析构函数的存在可以帮助我们进行资源的管理,防止内存泄露
1)析构函数的形式
~类名()
{
函数体
}
~Date()
{}
2)析构函数的特点
(1) 无参数无返回值;
(2) 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数;
(3) 对象生命周期结束时,编译器自动调用析构函数,完成资源的清理;
(4) 派生类对象先完成自定义成员的资源清理,再调用基类成员的析构函数来完成基类成员的清理;
代码演示:
#include <iostream>
using namespace std;
//基类
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
};
//派生类
class Derive :public Base
{
public:
Derive()
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
};
int main()
{
Derive d;
return 0;
}
运行结果演示:
(5) 析构函数不支持函数重载;
(6) 类中数据成员的析构顺序和声明顺序相反;
示例:
#include <iostream>
#include <climits>
#include <cstdlib>
using namespace std;
class Base1
{
public:
~Base1()
{
cout << "~Base1()" << endl;
}
};
class Base2
{
public:
~Base2()
{
cout << "~Base2()" << endl;
}
};
class Derive
{
private:
Base1 b1;
Base2 b2;
public:
~Derive()
{
cout << "Derive()" << endl;
}
};
int main()
{
Derive d;
return 0;
}
运行结果:
关于构造和析构的顺序,为什么构造的顺序是由外而内,而析构是由内而外?
因为要构造一个完成的对象需要先加载被依赖的部分,之后才能去调用被依赖的部分去初始化自身。析构时需要先清理不被依赖的部分,再清理被依赖的部分,这个顺序如果相反,就会导致被依赖部分已经清理掉,但是还可能继续被调用的情况出现。
3.拷贝构造函数
一种特殊的构造函数,通过一个已有的对象来生成一个新的对象
1)拷贝构造函数的形式
obj为一个形参,表示一个已存在的对象
类名(类名& obj)
{
函数体
}
Date(const Date& obj)
{}
2)拷贝构造函数的特点
(1)拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用问题;
(2) 如果不显示定义拷贝构造,编译器会自动生成拷贝构造函数,但编译器自动生成的拷贝构造只能完成资源的浅拷贝;
(3) 有资源就要显示定义拷贝构造;
编译器自动生成的拷贝构造只能进行字节序的拷贝,只能拷贝普通的成员,但不能拷贝资源,比如堆上的资源;
3)拷贝构造参数传值引发无穷递归的分析
//假设拷贝构造的形式是 Date(Date obj)
Date date;
Date copy(date);
要生成copy对象,就会调用拷贝构造,
obj作为形参,date作为实参,将date的值传递给obj,而在这个传值的过程中,date的值会生成一个匿名对象,这个匿名对象被用来构造obj,匿名对象又将自己的值传递给拷贝构造的形参,这个时候又开始了新一轮的拷贝构造,在新一轮的拷贝构造的过程中,始终无法将实参的值传递给形参,会陷入一种无穷递归的场景
图解如下:
4.赋值运算符重载函数
通过对赋值运算符=的函数重载让类对象之间可以相互赋值,就像自定义类型一样
1)赋值运算符重载函数的形式
类型名& operator=(类型名& obj)
{
return *this;
}
2)赋值运算符重载函数的特点
返回值类型必须为当前对象的引用;
必须在赋值之前进行判断是不是给自身赋值;
5.对普通对象取地址重载和对const对象取地址重载
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容
- 对普通对象取地址重载格式:
类型名* operator&()
{
return this;
}
- 对const对象取地址重载格式:
const 类型名* operator&() const
{
return this;
}
自定义返回值示例:
Date* operator&()
{
return nullptr;
}
const Date* operator&() const
{
return nullptr;
}
6.一个日期类的编写
通过日期类的编写来熟悉类的成员函数
class Date
{
public:
Date(int year = 2021, int month = 10, int day = 8)
:_year(year)
, _month(month)
, _day(day)
{
if (_year < 0 || _month < 1 || _month > 12 || _day <= 0
|| _day > GetMonthDay(_year, _month))
{
_year = 1;
_month = 1;
_day = 1;
}
cout << "Date()" << endl;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
cout << "Date(const Date& d)" << endl;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
~Date()
{
cout << "~Date()" << endl;
}
// 获取某年某月的天数
//输入不合法,返回-1
int GetMonthDay(int year, int month)
{
if (year < 0 || month < 1 || month > 12)
{
return -1;
}
//定义一个平年每个月份对应的天数数组,下标对应月份
int dayOfMonth[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
//判断是否为闰年
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
{
dayOfMonth[2] += 1;
}
return dayOfMonth[month];
}
// 日期+=天数
Date& operator+=(int day)
{
//先对天数进行累加
_day += day;
//累加之后天数为正
if (_day > GetMonthDay(_year, _month))
{
//如果天数超过了本月天数,更新月份甚至年份
while (_day > GetMonthDay(_year, _month))
{
//减去本月天数
_day -= GetMonthDay(_year, _month);
//月份+1
_month++;
//月份超出范围,年份更新
if (_month == 13)
{
_year += 1;
_month = 1;
}
}
}
else if (_day < 0)
{
//如果天数小于本月天数,更新月份甚至年份
while (_day < 0 && _day < GetMonthDay(_year, _month))
{
//加上本月天数
_day += GetMonthDay(_year, _month);
//月份-1
_month--;
//月份超出范围,年份更新
if (_month == 0)
{
_year -= 1;
_month = 12;
}
}
}
else
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day = GetMonthDay(_year, _month);
}
return *this;
}
// 日期+天数
Date operator+(int day)
{
Date copy(*this);
copy += day;
return copy;
}
// 日期-=天数
Date& operator-=(int day)
{
return *this += (-day);
}
// 日期-天数
Date operator-(int day)
{
Date copy(*this);
copy -= day;
return copy;
}
// 前置++
Date& operator++()
{
return *this += 1;
}
// 后置++,这个int型参数只是为了表示这是后置运算符
Date operator++(int)
{
Date copy(*this);
*this += 1;
return copy;
}
// 前置--
Date& operator--()
{
return *this -= 1;
}
// 后置--,这个int型参数只是为了表示这是后置运算符
Date operator--(int)
{
Date copy(*this);
*this -= 1;
return copy;
}
// ==运算符重载
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
// !=运算符重载
bool operator != (const Date& d)
{
return !(*this == d);
}
// >运算符重载
bool operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year < d._year)
{
return false;
}
//年份相同
else
{
if (_month > d._month)
{
return true;
}
else if (_month < d._month)
{
return false;
}
//月份相同
else
{
if (_day > d._day)
{
return true;
}
else
{
return false;
}
}
}
}
// >=运算符重载
inline 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);
}
// 日期-日期 返回天数
int operator-(const Date& d)
{
int day1 = 0;
int day2 = 0;
//计算每个日期对应的总天数
for (int i = 0; i < _month; ++i)
{
day1 += GetMonthDay(_year, i);
}
day1 += _day;
for (int i = 0; i < d._month; ++i)
{
day2 += GetMonthDay(d._year, i);
}
day2 += d._day;
return day1 - day2;
}
private:
int _year; //年
int _month; //月
int _day; //日
};
三.类的特殊成员
1.static成员
1)概念
用static修饰的成员称为静态成员,用static修饰的成员变量称之为静态成员变量,
用static修饰的成员函数称之为静态成员函数。
静态的成员变量一定要在类外进行初始化;
静态成员变量和静态成员成员函数示例:
class Test
{
public:
//静态成员函数
static int getInstance()
{
return _a;
}
private:
static int _a; //静态成员变量
};
int Test::_a = 1024; //静态成员在类外进行初始化
2) 特点
a. 静态成员为所有类对象所共享,不属于某个具体的实例;
也就是说静态成员只有一份,无论创建了多少个类对象,或者被多少子类继承;
代码实例分析:
#include <iostream>
using namespace std;
class Test
{
public:
//静态成员函数
static int* getInstance()
{
return &_a;
}
private:
static int _a; //静态成员变量
};
int Test::_a = 1024; //静态成员在类外进行初始化
class Derive :public Test
{
private:
int _b;
};
int main()
{
Test obj;
Test obj2;
Test obj3;
Derive obj4;
Derive obj5;
int* ret = obj.getInstance();
int* ret2 = obj2.getInstance();
int* ret3 = obj3.getInstance();
int* ret4 = obj4.getInstance();
int* ret5 = obj5.getInstance();
return 0;
}
运行监视:
通过监视结果可以看到,所有对象的静态成员都是同一个实体
b.整形静态常量可以在类内部进行初始化,也可以在类的外部初始化;
代码示例:
#include <iostream>
using namespace std;
class Base
{
public:
void show()
{
cout << "a: " << a << endl;
cout << "b: " << b << endl;
}
private:
static int a; //普通静态成员变量
static const int b = 3; //整形静态常量
};
int Base::a = 1;
//const int Base::b = 2;
int main()
{
Base obj;
obj.show();
return 0;
}
c. 类静态成员即可用类名::静态成员或者对象.静态成员来访问;
示例:
#include <iostream>
using namespace std;
class Test
{
public:
//静态成员函数
static int* getInstance()
{
return &_a;
}
public:
static int _a; //静态成员变量
};
int Test::_a = 1024; //静态成员在类外进行初始化
int main()
{
Test obj;
//类静态成员函数的两种访问方式
int* ret = obj.getInstance();
int* ret2 = Test::getInstance();
//类静态成员变量的两种访问方式
int instance = obj._a;
int instance2 = Test::_a;
return 0;
}
d. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员;
e. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值;
3)静态成员和非静态成员的调用关系
静态成员不可以调用非静态成员,因为没有this指针;
而非静态成员可以调用静态成员。
2. 友元
友元是一种定义在类外部的普通函数或类,但它需要在类体内进行说明,为了与该类的成员函数加以区别,在说明时前面加以关键字friend。
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
有时候当我们需要频繁地访问类中的私有和保护成员时,当访问这些成员的函数不是类的成员函数时,我们就可以将其声明为友元函数或者将这个类声明为友元类
友元的分类:
1) 友元函数
如果想为某个类外函数访问类中所有成员的权限,那就将它声明为友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
友元函数的使用说明
友元函数可访问类的私有和保护成员,但不是类的成员函数;
友元函数不能用const修饰;
友元函数可以在类定义的任何地方声明,不受类访问限定符限制;
一个函数可以是多个类的友元函数;
友元函数的调用与普通函数的调用和原理相同;
友元函数的声明与使用示例示例:
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 2021, int month = 10, int day = 9)
:_year(year)
, _month(month)
, _day(day)
{}
//将输出运算符重载函数声明为友元函数
friend ostream& operator<<(ostream& out, Date date);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, Date date)
{
out << date._year << " " << date._month << " " << date._day;
return out;
}
int main()
{
Date date;
cout << date << endl;
return 0;
}
2) 友元类
如果想让一个类A的所有成员函数都能访问另一个类B的私有和保护成员,那就将类A声明为类B的友元类。
3) 友元的特性
a. 友元关系是单向的,不具有交换性;
类A是类B的友元类,类A的成员函数可以访问类B的成员,但类B不能访问类A的成员,
也就是这只是一种单方面的认可
b. 友元关系不能传递;
如果B是A的友元,C是B的友元,则不能说明C是A的友元
c. 友元关系不能够继承;
父类的友元和子类没有任何关系。
使用示例:
#include <iostream>
using namespace std;
class Date
{
public:
//声明Time是Date的友元类,让Time的成员函数可以访问Date的所有成员
friend class Time;
Date(int year = 2021, int month = 10, int day = 9)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
class Time
{
public:
Time(int hour = 9, int minute = 57, int second = 59)
:_hour(hour)
, _minute(minute)
, _second(second)
{}
void showTime(Date date)
{
cout << date._year << " " << date._month << " " << date._day << " ";
cout << _hour << ": " << _minute << ": " << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Date date(2021, 10, 24);
Time time;
time.showTime(date);
return 0;
}
3. 内部类
如果一个类B定义在另一个类A的内部,这个类B就叫做内部类。
内部类和外部类是相互独立的;
内部类是外部类的友元类,外部类不是内部类的友元类;
内部类的成员函数可以访问外部类的成员,但是外部类的成员函数不能访问内部类的成员;
1)特性
- 内部类可以定义在外部类的任何访问权限修饰符修饰的位置
class Outside
{
public:
class Inside1
{
int _a;
};
protected:
class Inside2
{
int _b;
};
private:
class Inside3
{
int _c;
};
};
- 内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
class Outside { public: class Inside { public: void show(Outside obj) { cout << _b << endl; } private: int _c; }; private: int _a = 1; static int _b; //静态成员 }; int Outside::_b = 2;
编译报错:
- sizeof(外部类)=外部类,和内部类没有任何关系
示例:
#include <iostream> using namespace std; class Outside { public: class Inside { int _b; }; private: int _a = 1; }; int main() { //一个外部类的大小和内部类无关 cout << sizeof(Outside) << endl; //4 return 0; }
2)内部类的使用
可以通过外部类类名加上一个类作用域限定符::再加上内部类类名来声明一个内部类对象
示例:
#include <iostream> using namespace std; class Outside { public: class Inside { int _b; }; private: int _a = 1; }; int main() { Outside::Inside obj; return 0; }