目录
1.类的定义
class 类名{
//类体:
成员方法(成员函数)
成员变量
};//一定要加分号
class是定义类的关键字,{}中为类体,类体又包括成员方法和成员变量,注意类定义结束时最后的分号不可省略!
成员方法又称成员函数,即类中定义的函数;类中定义的变量就叫成员变量或成员属性。
类的两种定义方式:
- 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当作内联函数处理。
class Person{
public:
//吃饭——成员函数
void eating()
{
cout << “我爱吃饭!” << endl;
}
public:
//成员变量
char* _name;//姓名
char* _sex;//性别
int _age;//年龄
};
- 类声明放在.h文件中,成员函数定义在.cop文件中,注意:成员函数名前需加类名::
//声明放在类的头文件person.h中
class Person{
public:
void eating();
public:
char* _name;
char* _sex;
int _age;
};
//定义放在类的实现文件person.cpp中
#include”person.h”
void Person::eating()
{
cout << “我爱吃饭” << endl;
}
一般情况下,更期望采用第二种方式。
成员变量命名规则建议:
class Date{
public:
void Init(int year)
{
//这里的year到底是成员变量,还是函数形参?
year = year;
}
private:
int year;
};
//一般都这样写
class Date{
public:
void Init(int year)
{
_year = year;
}
private:
int _year;
};
//或者这样
class Date{
public:
void Init(int year)
{
year_ = year;
}
private:
int year_;//mYear
};
//其他命名也可以,主要是为了增加一个标识做以区分
2.类的限定符及封装
2.1访问限定符
C++实现封装的方式,用类将对象的属性和方法放在一起,让对象更加完善,通过访问限定符选择性地将其接口提供给外部的用户使用。
三种访问限定符:
- public(公有)
- protected(保护)
- private(私有)
【访问限定符说明】
- public修饰的成员是可以在类外直接被访问的。
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)。
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符的出现时为止。
- 如果后面没有访问限定符出现,作用域就到 } ,即类结束时。
- class的默认访问权限符为private,struct为public(因为struct要兼容C)。
注意:访问权限符只在编译时有用,当数据映射到内存后,没有任何访问权限符上的区别。
问:C++中struct和class的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用,另外C++中struct还可以用来定义类,和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。注意:在继承和模版参数列表位置,struct和class也有区别,后续给大家介绍。
2.2封装
面向对象的三大特性:封装、继承、多态。
在类和对象阶段,主要是研究类的封装特性,那么什么是封装?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更加方便使用类。
在C++语言中实现封装,可以通过将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
3.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域内在类体外定义成员时,需要使用::
作用域操作符指明成员属于哪个类域。
class person{
public:
void Info();
private:
char _name[20];
char _sex[3];
int _age;
};
void Person::Info()
{
cout << _name << “ ” << _sex << “ “ << _age << endl;
}
4.类的实例化
用类创建对象的过程,就叫类的实例化。
- 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
- 一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,存储类成员变量。
int main()
{
Person::_age = 20;//编译不通过,没有实例化对象就直接访问成员变量
return 0;
}
//正确用法
int main()
{
Person p1;
p1._age = 20;
Person p2;
Person* ptr = &p2;
ptr->_age = 19;
return 0;
}
5.类对象模型
5.1计算类对象的大小
class A{
public:
void PrintA()
{
cout << _a << endl;
}
private:
char _a;
int _b;
};
class A2{
public:
void PrintA2();
};
class A3
{};
int main()
{
A a1;
//8 和结构体一样,遵循内存对齐原则
cout << sizeof(A) << endl;
cout << sizeof(a1) << endl;
//1 占位,不存储有效数据,用于标识对象存在
cout << sizeof(A2) << endl;
cout << sizeof(A3) << endl;
}
一个类的大小,实际就是该类中“成员变量”之和,注意内存对齐
空类大小是一个字节,编译器给了空类一个字节来唯一标识这个类的对象。 打印对象的地址时,就可以看出,两个对象之间就是以一个字节做以区分。
5.2类对象的存储方式
只保存成员变量,成员函数放在公共的代码段。
5.3结构体内存对齐原则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编辑器默认的一个对齐数 与 该成员大小的较小值。
vs的默认对齐数是8 - 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数值。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构腿的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
6. this指针
this指针的定义和传递,都是编译器的活,我们不可以去抢,但我们可以在类里面使用this指针。
this指针的特性:
- this指针的类型:类类型*const,即成员函数中,不能给this指针赋值。
- 只能在“成员函数”内部使用。
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参,所以对象中不存储this指针。
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
- 形参,存储在函数栈帧里。
void datePrint()
{
cout << _year << “:” << _month << “:” << _day << endl;
}
void datePrint(Date* const this)
{
cout << this->_year << “:” << this->_month << “:” << this->_day << endl;
}
7.类的6个默认成员函数
任何类在什么也不写的时候,编译器会自动生成以下6个默认成员函数。空类就是这样。
默认成员函数:用户没有显示实现,编译器会生成的成员函数称为默认成员函数。
7.1初始化和清理
- 构造函数主要完成初始化工作
- 析构函数主要完成清理工作
7.2拷贝复制
- 拷贝构造是使用同类对象初始化创建对象
- 赋值重载主要是把一个对象赋值给另一个对象
7.3取地址重载
- 主要是普通对象和const对象取地址,这两个很少会自己实现
8.构造函数
8.1概念
构造函数是一个特殊成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合格的初始值,并且在对象整个生命周期里只调用一次。
8.2特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
特征:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
class Date{
public:
//1.带参构造构造
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//2.无参带缺省构造,与上面的不会同时存在
Date()
{
_year = 1970;
_month = 1;
_day = 1;
}
//3.无参构造
Date()
{}
//4.常用带缺省参数构造
Date(int year = 1970, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用无参构造函数
Date d2(2023, 8, 23);//调用带参构造函数
}
- 如果类中没有显式定义构造函数,则C++编译器会自动申城一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
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;
};
int main()
{
// 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函
数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再
生成
// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
Date d1;
return 0;
}
- 编译器自动生成的默认构造函数,当对象调用这个默认构造函数时,该对象的成员变量值时随机的。那难道编译器生成的默认构造函数就没有用吗?
解答:C++把类型分成内置类型(基本类型)和自定义类型,内置类型就是语言提供的数据类型,如:int/char….,自定义类型就是我们使用class/struct/union等自己定义的类型。现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值。
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
- 无参的构造函数和析构函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数,全缺省构造函数,我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。不传参数就可以调用的构造函数,就叫默认构造函数。
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
9.析构函数
9.1概念
析构函数:与构造函数功能相反,析构函数不是完成对象本身的销毁,局部对象销毁工作是由编译器完成的,而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
9.2特性
- 析构函数名是在类名前加上字符~。
~Stack() {}
- 无参数无返回值类型。
- 一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
- 对象生命周期结束时(出作用域),C++编译系统自动调用析构函数。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s;
s.Push(1);
s.Push(2);
}
- 编译器生成的默认析构函数,对自定义类型成员调用它的析构函数。
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
10.拷贝构造函数
10.1概念
拷贝构造函数:只有单个形参,该形参对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
10.2特性
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
class Date{
public:
Date(int year = 1970, 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(const Date d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
-
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。 -
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己实现吗?
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。 -
拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
尽量使用引用。
11.赋值运算符重载
11.1运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数, 也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this.
.*
,::
,sizeof
,?:
,.
注意以上5个运算符不能重载。
// 全局的operator==
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;
};
// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
void Test ()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout<<(d1 == d2)<<endl;
}
class Date{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year;
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
11.2 赋值运算符重载
- 赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
class Date{
public :
Date(int year = 1900, 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 ;
};
- 赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。
- 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
class Time{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2;
d1 = d2;
return 0;
}
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实
现吗?当然像日期类这样的类是没必要的。
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必
须要实现。
11.3前置++和后置++重载
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()
{
_day += 1;
return *this;
}
// 后置++:
// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器
自动传递
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this+1
//而temp是临时对象,因此只能以值的方式返回,不能返回引用
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
Date d1(2022, 1, 13);
d = d1++; // d: 2022,1,13 d1:2022,1,14
d = ++d1; // d: 2022,1,15 d1:2022,1,15
return 0;
}
12.日期类的实现
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
static int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30,
31};
int day = days[month];
if (month == 2&&((year % 4 == 0 && year % 100 != 0) || (year%400 == 0)))
{
day += 1;
}
return day;
}
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
// 检查日期是否合法
if (!(year >= 1
&& (month >= 1 && month <= 12)
&& (day >= 1 && day <= GetMonthDay(year, month))))
{
cout << "非法日期" << endl;
}
}
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d)
{
if (*this != d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
// 析构函数
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
// 日期+=天数
Date& operator+=(int day)
{
if (day < 0)
{
return *this -= abs(day);
}
_day += day;
while (day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
// 日期+天数
Date& operator+(int day)
{
Date ret(*this);
ret += day;
return ret;
}
// 日期-天数
Date operator-(int day)
{
Date ret(*this);
ret -= day;
return ret;
}
// 日期-=天数
Date& operator-=(int day)
{
if (day < 0)
{
return *this += abs(day);
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// 前置++
Date& operator++()
{
*this += 1;
return *this;
}
// 后置++
Date operator++(int)
{
Date ret(*this);
*this += 1;
return ret;
}
// 后置--
Date operator--(int)
{
Date ret(*this);
*this -= 1;
return ret;
}
// 前置--
Date& operator--()
{
*this -= 1;
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 _year == d._year
&& _month == d._month
&& _day == d._day;
}
// >=运算符重载
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;*/
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 operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++n;
++min;
}
return n * flag;
}
private:
int _year;
int _month;
int _day;
};
13.const成员函数
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
void Print() const
{
cout << "Print()const" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
void Test()
{
Date d1(2022,1,13);
d1.Print();
const Date d2(2022,1,13);
d2.Print();
}
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
14.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!