目录
1.类和对象
1.1类的定义
类的定义格式
class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意定义结束时后面分号不省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或成员函数。
//text.cpp #include<iostream> using namespace std; class Stack { //成员变量 int* a; int top; int capacity; //成员函数 void Push() { } void Pop() { } };//分号不能省略 int main() { return 0; }
- 为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或后面加_或者m_开头。这个C++语法上并没有规定,仅凭个人或公司喜好
//为区分成员变量,一般前面加_ //成员变量 int* _a; int _top; int _capacity;
- C++中struct也可以定义类,C++兼容c中struct的用法,同时struct升级成了类,明显的变化是struct中也可以定义函数,一般情况下我们还是推荐用class定义类
- 定义在类里面的成员默认为inline
1.2访问限定符
C++一种实现封装的方式,用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
- public(公有)修饰的成员在类外可以直接被访问,protected(保护)和private(私有)修饰的成员在类外不能直接被访问,protected和private是一样的
- 访问权限作用域从该访问权限出现的位置开始直到下一个访问限定符出现为止,如果后面没有访问限定符,作用域就到 } 即类结束
//text.cpp #include<iostream> using namespace std; class Stack { /// void Push() { } //Push 没给限定符 class默认私有 private /// public: void Pop() { } int Swap() { } //Pop和Swap 被public修饰,直到下一个限定符出现之前都为公有 /// protected: int add(); //add 被public修饰,直到下一个限定符出现之前都为保护 /// / private: int* _a; int _top; int _capacity; //成员变量被private修饰,直到}结束都为私有 }; int main() { Stack st; //公有可以访问 st.Pop(); st.Swap(); //私有不可访问 st._top; return 0; }
额外注意:
- class定义成员没有被访问限定符修饰时默认为private,struct默认为public
- 一般成员变量都会被限制为private/protected,需要给别人使用的成员函数会被放为public
1.3类域
类定义了一个新的作用域,类所有成员都在类的作用域中,在类体外定义成员时,需要使用 ::作用域操作符指明成员属于哪个类域
类域影响的是编译的查找规则,下面程序中Init如果不指定类域Stack,那么编译器就会把Init当成全局函数,那么编译时找不到_top等成员,就会到类域去找
//text.cpp #include<iostream> using namespace std; class Stack { public: void Init(int x, int y); }; void Stack::Init(int x, int y) { _top = x; _capacity = y; } int main() { return 0; }
注意:
- 类里面的函数声明定义分离,类创建后形成了新的类域,需要指定类域,否则不可访问
2.实例化
2.1实例化概念
- 用类型在物理内存中创建的过程,称为类实例化出对象
- 类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会去分配空间
//text.cpp #include<iostream> using namespace std; class Stack { //声明 int* _a; int _top; int _capacity; }; int main() { Stack::_top = 2024; //编译器报错,_top只是声明,并未实例化 return 0; }
- 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储成员变量。打个比方:类实例化出对象就像现实中使用建筑设计图造房子一样,类就像设计图,设计图规划出有多少个房间,房子大小等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像设计图一样,只是告诉编译器即将要开多大的内存,但是不开内存,只有实例化出的对象才分配物理内存存储数据
-
//text.cpp #include<iostream> using namespace std; class Stack { //声明 int* _a; int _top; int _capacity; }; int main() { Stack st; st._top=2024; //Stack实例化出st,系统已经给st分配内存了,可以存储数据,编译通过 return 0; }
2.2对象大小
- 分析一下类对象都有哪些成员呢?类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?首先函数被编译后是一段指令,对象中没法储存,这些指令存储在一个单独的区域(代码段),那么对象非要存储的话,只能是成员函数的指针。对象中是否有存储指针的必要呢,Date实例化出两个对象d1和d2,di和d2都有各自独立的成员变量_year/_month/_day存储各自的数据,但是d1和d2的成员函数Init/Print指针却是一样的,存储在对象中就浪费了。如果用Date实例化出100个对象,那么成员函数指针就重复存储100次,太浪费了。其实函数指针不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call地址],其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址。
内存对齐规则
- 第一个成员在与结构体偏移量为0处的地址处
- 其他成员变量要对齐对齐数的整数倍的地址处
- 对齐数=编译器默认的对齐数与该成员的大小的较小值
- VS x64平台默认对齐数是4,x86默认对齐数是8
- 结构体总大小为:最大对齐数(所有类型变量最大者与默认对齐数取最小)的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍,结构体整体大小就是所有最大对齐数(含嵌套结构体对齐数)的整数倍
class A { public: void Print() { cout << _ch << endl; } private: char _ch; int _i; }; //_ch 是一个字节,默认对齐数是4,最大对齐数是4,所以开辟4个字节用来存在_ch // _i是4个字节,默认对齐数是4,最大对齐数是4,所以开辟4个字节用来存储_i class B { public: void Print() { //。。。 } }; class B { }; //B和C里面没有存储任何成员变量,只有一个函数,可成员函数不存对象里面 // 按理来说是0,但是结构体怎么会没大小,为表示对象存在C++对这种规定大小为1,为了占位标识对象存在
3.this指针
编译器编译后,类的成员函数默认都会在形参第一个位置,增加一个当前类的指针,叫做this指针
例如Date类中的Init原型为 void Init(Date * const this,int year ,int month,int day),类的成员函数中访问成员变量,本质是通过this指针访问的,如Init函数中给_year赋值,this->_year=year
原型:
class Date { void Print() { cout << _year << "\n" << _month << "\n" << _day << endl; } void Init( int year, int month,int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
Date d1; d1.Init(2024,7,10); d1.Print(); Date d2; d2.Init(2024, 7, 9); d2.Print();
真实原型
class Date { void Init(Date* const this,int year, int month,int day) { this->_year = year; this->_month = month; this->_day = day; } void Printf(Date* const this) { cout << this->_year << "\n" <<this-> _month << "\n" << this->_day << endl; } private: int _year; int _month; int _day; };
Date d1; d1.Init(&d1,2024,7,10); d1.Print(&d1); Date d2; d2.Init(&d2,2024, 7, 9); d2.Print();
C++规定不准在实参和形参的位置写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针,this指针不能修改,但this指针指向的内容可以
this指针存在栈里
4.类的默认成员函数
默认成员函数就是用户没有显示定义,编译器会自动生成的成员函数称为默认成员函数
4.1构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名字叫构造函数,但是构造函数的主要内容并不是开辟空间创造对象(我们平常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美替代了Init
构造函数的特点:
- 函数名与类名相同
- 无返回值(返回值啥也不需要给,也不要写void C++就是这样规定)
- 对象实例化时系统会自动调用对应的构造函数
- 构造函数可以重载
- 如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成
class Date {public: //1.无参构造函数 Date() { _year = 1; _month = 1; _day = 1; } //2.带参构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } //3.全缺省构造函数 Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
无参构造函数,全缺省构造函数,我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个有且只能有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。注意并不是只有默认构造函数就是编译器默认生成的那就是构造函数,无参构造函数,全缺省构造函数也是默认构造函数,总结一下就是不传参就能调用
我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。
//text.cpp #include<iostream> using namespace std; typedef int STDataType; class Stack { public: Stack(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr == _a) { perror("malloc申请失败"); } _capacity = n; _top = 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; //两个Stack实现队列 class MyQueue { private: int size; Stack pushst; Stack popst; }; int main() { MyQueue my; return 0; }
C++把类型分为自定义类型和内置类型(基本类型)。内置类型就是语言提供的原生数据类型,如int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义的类型。这里构造函数自动初始化,VS也将内置类型size初始化了,不同的编译器初始化值不同,C++并没有规定
对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认的构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决
总结:大多数情况下,构造函数都需要我们自己去实现,少数情况类似MyQueue且Stack有默认构造函数时,MyQueue自动生成就可以用
4.2析构函数
~Stack() { free(_a); _a = nullptr; _top = _capacity = 0; }
析构函数的特点:
1.析构函数名是在类名前面加上字符~
2.无参无返回值(与构造函数一致)
3.一个类只能有一个析构函数。若未显示定义,系统也会自动生成默认的析构函数
4.对象声明周期结束时,系统会自动调用析构函数,
5.跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用其他的析构函数
6.还需注意的是我们显示析构函数,对于自定义类型成员也会调用他的析构函数,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
//text.cpp #include<iostream> using namespace std; typedef int STDataType; class Stack { public: Stack(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr == _a) { perror("malloc申请失败"); } _capacity = n; _top = 0; } ~Stack() { free(_a); _a = nullptr; _top=_capacity=0; } private: STDataType* _a; size_t _capacity; size_t _top; }; //两个Stack实现队列 class MyQueue {public: //编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化 //编译器默认生成MyQueue的析构函数调用了Stack的析构,释放了Stack内部的资源 //显示写析构也会调用Stack的析构 ~MyQueue() { cout << "~MyQueue" << endl; } private: Stack pushst; Stack popst; }; int main() { MyQueue my; return 0; }
MyQueue里的析构啥也没干,但是C++规定会调用其他的析构来释放内存
如果没有申请资源时,析构可以不写,直接使用编译器生成的默认析构函数,如Date,如果默认生成的析构可以用,也就不需要显示写析构如MyQueue,但是有资源申请时,一定要直接写析构,否则会造成资源泄漏如Stack
4.5运算符重载
- 当运算符被用于类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有则编译器报错
- 运算符重载是具有特定名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体
bool operator<(Date d1, Date d2) { } bool operator==(Date d1,Date d2) { return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day; }
- 重载运算符函数的参数个数和该运算符作用的参数一样多。一元运输安抚有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数
//text.cpp #include<iostream> using namespace std; class Date { public: Date(int year, int month, int day) { _year= year; _month = month; _day = day; } int _year; int _month; int _day; }; int GetMonthDay(int year ,int month) { static int monthDayArray[13]={-1,31,28,31,30,31,30,31,31,30,31,30,31}; if(month==2&&((year%4==0&&year%100!=0)||(year%400==0) { return 29; } return monthDayArray[month]; } bool operator<(Date d1, Date d2) { Date tem=*this; tem-=day; return tmp; } //天数是否相等 bool operator==(Date d1,Date d2) { return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day; } //日期-天数 Date& Date:operator-=(int day) { _day-=day; while(_say<=0) { --_month; if(_month==0) { _month=12; --_year; } } _day+=GetMonthDay(_year,_month); } //Date 加一个天数 Date& Date::operator+=(int day) { _day+=day; while(_day>GetMonthFay(_year,_month)) { _day-=GetMonthDay(_year,_month); ++month; if(_month==13) { _year++; _month=1; } } return *this; ) //日期比大小 bool Date::operator<(const Date& d) { if(_year<d._year) { return ture; } elae if(_year==d,_year) { if(_month<d._month) { return true; } else if(_month==d._month) { return _day<d._day; } } return false; } //日期比大小 bool Date :: operator<=(const Date&d) { return *this<d||*this==d; } //日期比大小 bool Date :: operator>=(const Date&d) { return !(*this<=d); } int main() { Date d1(2024, 7, 10); Date d2(2024,7,9); //两种用法都可以 d1 == d2; operator==(d1 , d2); return 0; }
- 如果一个重载运算符函数是成员函数,则他的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个
- 运算符重载以后,其优先级和结合性与内置类型运算保持一致
- 不能通过连接语法中没有的符合来创建性的操作符:比如operator@
- .* :: sizeof ?: . 以上五个不能重载
- 一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意义,operator+就没有意义
取地址运算符重载
const 成员函数
- 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面
void Print() const { cout << _year << _month << _day << endl; }
- const 实际修饰该成员函数隐含的this指针,表明该成员函数中不能对类的任何成员进行修改。const修饰Date类的Print成员函数,Print隐含的this指针由Data* const this变成为const Data* const this
取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般两个函数编译器自动生成的就够我们用,不需要自己去实现。除非一些很特殊的场景,比如说我们不想被别人取到当前类对象的地址,就可以自己去实现一份,胡乱返回一个地址
Date* Date :: operator&(const Date & d) { return (Date*)0x15612FE40; } const Date* Date :: operator&(const Date& d) const { return (Date*)0x15612FE30; }