02--类和对象
💡 面向过程和面向对象的初步认识
- c语言: 面向过程 关注的是求解问题的步骤
- cpp: 面向对象 关注的是一个问题有哪几个对象
1. 类
1.1. 类的概念
类: 一种描述对象属性(成员变量) 和 行为(成员方法)的抽象.
1.2. 类的声明
两种定义的区别:
- struct兼容c的语法(c语法升级),class是一种新的语法
- struct默认成员是公开,class默认成员是私有
成员变量命名规范:一般前置下划线
原因在于: 避免标识符冲突
结论:在成员函数中,有形参就优先访问形参,没有形参就访问成员函数
1.3. 类的定义(实例化)
类对象实例化:按照类 在内存中实际开辟空间的过程
类对象: 类实例化后所产生的对象.
类定义的区分:
Date;
类型, 没有实际意义, 编译器忽略处理.Date();
匿名类对象, 有实际的空间, 生命周期仅为这一行.Date d1();
函数声明, 标识存在一个返回类型为Date, 参数为空的函数Date d2(2024, 10, 23);
类对象的定义, 调用的是有参构造Date d3()
类对象的定义, 调用的无参构造(默认构造)
1.4. 类的访问
类对象实例化:按照类 在内存中实际开辟空间的过程
1.4.1. 访问限定 -- 封装
封装:将类中的成员变量/函数通过针对性的对外隐藏所实现的一种隐藏细节的管理方式
- 首先,封装这种管理方式是在类中进行的,也就是说明类也是一种域,类定义了一个新的作用域,我们称为"类域",如果要想在类外定义成员,需要用到符号"::" 来进行指定。
- 其次,封装这种管理方式是利用访问限定符来实现的。
- 类遵循面向对象的封装思想:即通过类,合适的隐藏对象的属性和实现细节,仅对外公开一定的接口来实现和其他对象的交互。
1.4.2. 程序封装的意义?
能够有效规范化程序,提高代码健壮性, 从而降低程序的规范度对程序员素质的依赖.
1.4.3. 类的访问格式
语法格式:类对象 + 点 + 成员变量/成员函数
1.5. 类对象模型
类对象模型:描述类对象在内存中存储的抽象模型。
结论:类对象中的成员变量是分开存储的,类对象中的成员函数是共用同一块区域的。
1.6. 类对象大小的计算
类对象大小由类对象成员变量 决定且遵循 内存对齐 原则。
内存对齐的意义? 为了CPU效率的考量.
- 硬件规定,CPU内存堆去每次读取4/8个字节
- 且需要从内存整数倍处读
倘若不进行内存对齐的话,可能会出现一个变量需要读两次然后进行组合的情况,这样大大降低了CPU内存读取的效率。
1.7. this指针
this指针: 隐藏于每个一般成员函数中的, 用来指向自己类对象的指针.
特点:
- 我们不能把this写出来,这工作属于编译器
- this可以在函数内部使用
- this的指向不可更改
Date* const this;
this指针的存储位置 -- 栈区(函数形参)
this指针 是如何拿到类对象地址的?
理解1: 访问类成员变量, 必须通过this指针访问
类对象的成员变量是存放在类对象地址指向的区域的,因而需要有效的地址
理解2: 访问类成员函数, 不需要this指针
类对象的成员函数是存放在公共代码区的,调用此函数不需要类对象地址。
理解3: nullptr指针也可以调用类内函数
答案是B.
- 第一处不会崩溃, 因为不需要this指针即可调用
print()
,print()
存在于公共区域 - 第二处会崩溃,
_a
存储在类对象分离区域(栈上), 每个类对象独立, 此时this = nullptr
,*this
是未定义行为 - 没有
p->print()
, 第三处可以调用print()
成功, 但是一旦访问_a就会崩溃(原因同2).
1.8. 匿名类对象
匿名类对象: 没有名字的, 声明周期只有定义处一行代码 的特殊类对象。
- 匿名类对象的写法是Type();
- Date(); // 匿名类对象
- Date d(); // 函数声明
- 匿名类对象的生命周期仅限所在的一行
- 匿名类对象的应用:
Solution().Sum_Solution(lo);
2. 默认成员函数
2.1. 默认成员函数概念
默认成员函数: 在自己不写的情况下,编译器默认生成的成员函数
- 初始化和清理
- 自己不写,编译器自动生成
- 对于内置类型忽略,对于自定义类型会去调用其对应的构造/析构函数
- 拷贝赋值
- 自己不写,编译器会自动生成
- 对于内置类型会拷贝/赋值,对于自定义类型会去调用其对应的拷贝/赋值函数
- 取地址重载
- 一般自己不用写
2.2. 构造函数
引入:cpp规定,每个类实例化时候必须初始化,而这个工作是由构造函数完成的
构造函数的概念:完成类对象初始化工作的特殊成员函数
构造函数的特性:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。一般建议实现一个全缺省的构造函数。
class Data
{
private:int __year;int __month;int __day;public:Data() // 构造函数{__year = 0;__month = 0;__day = 0;}Data(int year = 1, int month = 1, int day = 1) {__year = year;__month = month;__day = day;}void DataPrint() {cout << __year << "/" << __month << "/" << __day << endl;}
};void test1_1()
{Data d1;d1.DataPrint();
}
如果忘记定义构造函数怎么办?
编译器会自动生成无参构造器,VS下自己写的构造函数,即使里面什么都没有,编译器也会给你初始化为零。但需要注意的是,如果自己定义了一个构造函数之后,编译器不再默认生成无参构造函数。
默认构造函数与无参构造函数?
默认构造函数包括无参构造函数。
默认构造函数:编译器自动生成的构造函数、无参构造函数、全缺省的构造函数
- note1: 无参构造函数与全缺省的构造函数不能同时存在,原因在于调用会出现歧义。
- note2: 默认生成的构造函数不会初始化内置类型(标准未规定)
编译器会自动生成并调用的默认构造函数。
但是,默认构造函数对内置类型无视初始化,对自定义类型调用其默认构造函数。
2.3. 析构函数
析构函数: 完成类对象资源清理工作的特殊成员函数(并不是所有类都需要析构函数)
class Stack
{
private:int* _arr;int _size;int _capacity;public:void Init(int n = 4){int* temp = (int*)malloc(sizeof(int) * n);if (temp == NULL){perror("malloc fail");exit(-1);}_arr = temp;_size = 0;_capacity = n;}void Push(int x){_arr[_size++] = x;}~Stack(){free(_arr);_arr = nullptr;cout << "~Stack" << endl;}
};// -------------------------------------------------------
class Data
{
private:int _year;int _month;int _day;public:Data(int year = 1, int month = 1, int day = 1){cout << this << endl;_year = year;_month = month;_day = day;}void DataPrint(){cout << _year << "/" << _month << "/" << _day << endl;}~Data(){cout << "~Data" << endl;}
};
析构函数的特性:
- 析构函数名是在类名前加上字符~.
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,c++编译系统自动调用析构函数。
如果忘记写析构函数怎么办?
答: 编译器会自动生成默认析构函数。但是需要注意的是,如果自己定义一个析构函数之后,编译器就不会再生成。默认生成的析构函数不会清理内置类型,但是自定义类型的尽头是内置类型。
那该如何理解析构函数是否需要自己写问题呢?
对于自己申请了堆空间的类, 是需要写析构函数的, 一般没有申请堆空间的类, 是不用写的.
析构函数的销毁顺序(小重点)
销毁顺序:局部变量 --> 局部静态变量 --> 全局变量
2.4. 拷贝构造函数
- 拷贝构造函数的概念:用于拷贝类对象的特殊成员构造函数
注:CPP规定,自定义类型的拷贝需要调用拷贝构造函数
- 拷贝构造函数的特性:
- 自己不写,编译器自动生成。
- 默认拷贝构造函数会对类对象进行值拷贝(浅拷贝)
拷贝构造函数参数写引用的原因:避免无线递归问题
那为什么不用指针来作为拷贝构造的参数?
理论上是可行的,但实际上编译器会把我们用指针写的拷贝构造函数识别成构造函数重载。
思考:那我们是不是可以不用自己写拷贝构造函数了?
答:不是。因为有些场景下进行浅拷贝会出问题,比如深拷贝的情况。
比如以下情景,两个对象指向同一块空间,第一个对象析构制空没事,第二个对象
进行释放空间时候发现一块空间被释放两次会崩溃。
思考:什么情况下可以不用写拷贝构造函数?
答:都是值拷贝的情况。
为什么建议拷贝构造函数参数前面用const进行修饰引用???
答:一种代码健壮性习惯, 主要是防止拿来拷贝构造的样本被反修改了。
深浅拷贝对比
一般而言,需要用到动态开辟资源的情况,需要用到深拷贝,单纯拷贝值,自动生成的拷贝构造函数就可以完成工作。
// 栈的深拷贝与浅拷贝,浅拷贝的反例。#include<iostream>
using namespace std;
class Stack
{
private:int* arr;int top;int capacity;
public://构造函数Stack(int n = 10){int* t_arr = (int*)malloc(sizeof(int) * n);if (t_arr == nullptr){perror("malloc fail");return;}this->arr = t_arr;this->capacity = 10;this->top = 0;}//析构函数~Stack(){free(this->arr);}//拷贝构造函数//1.值拷贝error/*Stack(Stack& st){this->arr = st.arr;this->capacity = st.capacity;this->top = st.top;}*///2.深拷贝rightStack(Stack& st){int* t_arr = (int*)malloc(sizeof(int) * 10); // 自己开空间, 然后把对应的值送进去. if (t_arr == nullptr){perror("malloc fail");return;}this->arr = t_arr;this->capacity = st.capacity;this->top = st.top;}
};void test1()
{Stack s1;Stack s2(s1);
}int main()
{test1();return 0;
}
拷贝构造编译器优化问题
实际上, 老师说这个地方是两次拷贝构造优化为一次拷贝构造, 我现在试了一下, 他直接把一次构造 + 两次拷贝构造优化为一次构造(VS2022 版本下)
下图是 VC6.0 截图:
下图是 VS2022 下的截图:
下面是老师的讲解(讲解的是两次拷贝构造优化为一次拷贝构造):
2.5. 运算符重载函数
- 概念:允许自定义类型进行运算的特殊成员函数
- 定义: 函数原型:
returnType operator signal(参数列表)
函数名换成运算符的意义是什么?
答: 方便使用。
这种函数适合定义在类内还是类外,为什么?
答: 类内,定义在类外需要提供get函数或者是公开类成员变量。这与CPP中的封装理念相违背。
为什么这里的判断==函数只有一个d2参数,d1呢?
答: 实际上是因为默认类内的函数都有一个this指针,所以这里把date&d1换成了隐含的this指针而已.
运算符重载的注意事项
- 不能通过连接其他符号来创造新的操作符:比如operator@
- 重载操作符必须有一个 类类型参数
- 建议: 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义 注: 对于该条规定,主要是一条代码规范,不是必须的。
- 作为类成员函数重载时,其形参看起来比操作数数目少1, 因为成员函数的第一个参数为隐藏的
this
.*
::
sizeof
?:
.
注意以上5个运算符不能重载。这个经常在笔试选择题中出现.- 对于赋值运算符, 如果自己不写且用到了自定义类型的赋值函数,编译器会自动生成一个,其行为是: 对默认类型进行赋值,对自定义类型会去调用其对应的赋值函数
- 赋值运算符必须是成员函数。
//V1:赋值运算符重载
void operator(Date& d)
{this->day = d.day;this->month = d.month;this->year = d.year;
}
// 问题:连续赋值的情况怎么办?//V2:
Date operator=(Date& d)
{this->day = d.day;this->month = d.month;this->year = d.year;return *this;
}
// 问题:报错。原因是返回类型是Date,编译器会返回一个临时变量,这歌临时变量具有常性,常量值不能进行连续赋值
// 返回Date类型的话是拷贝赋值,效率比较低//V3:
Date& operator=(const Date& d)
{this->day = d.day;this->month = d.month;this->year = d.year;return *this;
}
// 问题不大。既给参数加上了const,防止被反赋值,又把返回类型改为了引用类型,这样返回的就不是一个常量,而是一个变量,这也就支持了连续赋值的问题
// 这里需要指出的是,连续赋值要求返回变量本身,而不是一个常量值。 eg:
// int i = 1, j = 0;
// (i = j) = 10;
// answer: i = 10, j = 0;//V4:
Date& operator=(const Date& d)
{if(this == &d){return *this;}this->day = d.day;this->month = d.month;this->year = d.year;return *this;
}
// 这样就是精益求精了,防止有些人喜欢自己给自己赋值从而降低效率。
思考:拷贝构造与赋值运算重载函数的区别是什么?
很明显两者有很大的区别,不然就不会分开说这两个函数了。
- 拷贝构造:同类型的已存在对象去初始化正在创建的一个对象。
- 赋值运算符函数重载:是两个已经存在的对象,拷贝赋值给另一个。
强制构造默认构造函数
注意,如果写了拷贝构造,那么编译器不再为我们生成默认构造函数,如果写了一般的构造函数,那么编译器仍然会给我们生成拷贝构造函数。
我们只写了拷贝构造,那可以强制让编译器生成一个默认拷贝构造函数吗?可以!
class A
{
public:int _a;A() = default;//强制生成默认构造函数A(const A& a):_a(a._a){}
};void test5()
{A a;
}
2.6. Date日期类的实现
问题探讨: 自定义类型运算符可以进行重载,+和+=之间可以相互复用, 谁复用谁更好?
思考:用+=复用+好?还是用+复用+=好?
- +=复用
- +复用+=
结论:+复用+=好。
- 用十二复用+是不好的,因为+要用一个临时date变量,+=复用+导致+也会生成一个临时变量。
- 用+复用+是较好的,因为+要用一个临时date变量,但是+=复用的十二就不会生成一个临时变量了。
#include"Date.h"Date::Date(int year, int month, int day)
{this->_day = day;this->_month = month;this->_year = year;if (!CheckInvalid()) cout << "error,reprint" << endl; // 检查合法性
}// 打印函数
void Date::DatePrint() const // 设置为const函数, 可以是的const和非const共用.
{cout << "year:" << this->_year << " ";cout << "month:" << this->_month << " ";cout << "day:" << this->_day << endl;
}bool Date::operator<(const Date& d)
{if (this->_year < d._year) return true;else if (this->_year == d._year){if (this->_month < d._month) return true;else if (this->_month == d._month)if (this->_day < d._day) return true;}return false;
}bool Date::operator<=(const Date& d)
{return (*this == d) || (*this < d);
}bool Date::operator>(const Date& d)
{return !(*this <= d);
}bool Date::operator>=(const Date& d)
{return !(*this < d);
}bool Date::operator==(const Date& d)
{return this->_year == d._year && this->_month == d._month && this->_day == d._day;
}bool Date::operator!=(const Date& d)
{return !(*this == d);
}Date& Date::operator+=(const int day)
{this->_day += day;//进位while (this->_day > GetMonthDay(this->_year, this->_month)){// 月进位 this->_day -= GetMonthDay(this->_year, this->_month);this->_month++;if (this->_month == 13) // 年进位{_year++;_month = 1;}}return *this;
}Date Date::operator+(const int day)//思考:这个地方为什么不用引用返回?答:因为我们返回的是一个栈内的临时Date,出函数会销毁。
{//Date temp(*this);Date temp = *this;//思考:这里是拷贝构造还是赋值?答:拷贝构造。为什么?因为拷贝构造与对象的诞生一同进行,而赋值是对于已经存在的对象而言的。temp._day += day;//进位while (temp._day > GetMonthDay(temp._year, temp._month)){temp._month++;temp._day -= GetMonthDay(temp._year, temp._month - 1);if (temp._month == 13){_year++;_month = 1;}}return temp;
}////+复用+=
//Date Date::operator+(const int day)
//{
// Date temp = *this;
// temp += day; // 进位相关的工作都在+=里面做了.
//
// return temp;
//}////+=复用+
//Date& Date::operator+=(const int day)
//{
// *this = (*this + day);
//
// return *this;
//}Date& Date::operator++()//前置++
{*this += 1;return *this;
}Date Date::operator++(int)//后置++
{Date temp = *this;*this += 1;return temp;
}int Date::operator-(const Date& d)
{Date max = *this;Date min = d;// 确定大小, 大 - 小. if (*this < d){max = d;min = *this;}int sub = 0;while (min != max){++min;sub++;}return sub;
}// 流插入
ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}// 流提取
istream& operator>>(istream& in, Date& d)
{while (true){in >> d._year >> d._month >> d._day;if (!d.CheckInvalid()){cout << "error, refail" << endl;continue;}else break;}return in;
}bool Date::CheckInvalid()
{if (_year <= 0 || _month < 1 || _month>12 || _day<1 || _day>GetMonthDay(_year, _month)) return false;else return true;
}
#pragma once
#include<iostream>
#include<assert.h>using namespace std;class Date {
private://友元函数friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);int _day;int _month;int _year;
public:Date(int year = 1, int month = 1, int day = 1);void DatePrint() const;bool operator<(const Date& d);bool operator<=(const Date& d);bool operator>(const Date& d);bool operator>=(const Date& d);bool operator==(const Date& d);bool operator!=(const Date& d);Date& operator+=(const int day);Date operator+(const int day);Date& operator++();//前置++Date operator++(int);//后置++int operator-(const Date& d);//算两个日期相差多少天bool CheckInvalid();//bool operator=(const Date& d);inline bool leap_year(int& year)//频繁调用,类内函数自动为inline{if (((year % 4 == 0) && year % 100 != 0) || year % 400 == 0){return true;}else{return false;}}inline int GetMonthDay(int year, int month)//频繁调用,类内函数自动为inline(可以不用加inline){assert(month >= 1 && month <= 12);static int months[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//细节:改静态if (month == 2 && leap_year(year))//细节:左右一换return months[month] + 1;return months[month];}/*Date(Date& d){cout << "拷贝构造" << endl;}*/
};//实现在类外作为全局函数
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
#include"Date.h"void test1()
{/*Date d1(9,1,9);Date d2(6,8,3);Date d3(10,8,9);cout << (d1 <= d2) << endl;cout << (d1 < d3) << endl;Date d4(2024, 4, 20);*//*Date d5(2023,2,29);d5 += 20;d5.DatePrint();*//*Date d6(1, 2, 3);Date d7(d6+=10);d7.DatePrint();d6.DatePrint();*//*Date d8(2024, 4, 20);Date d9(2023, 3, 31);int sub = d8 - d9;cout << sub << endl;*/
}void test2()
{Date d1;Date d2(2, 2, 2);//cout << d1;//写法1://d1 << cout;//奇怪的写法//写法2://d1.operator<<(cout);cin >> d1 >> d2;cout << d1 << d2;}void test3()
{/*Date d1(2024, 4, 21);d1.DatePrint();*/const int a = 10;//1.拷贝int b = a;//2.权限放大int& c = a;
}int main()
{//const 成员函数的认识test3();//test1();//test2();
}
2.7. const成员函数
const成员函数概念:const修饰this指针的指向内容,使this不能改变其指向内容的成员函数
说的直白一点, 你用 const 修饰的函数, 函数不是有个 this 形参嘛, 就等于 const 修饰这个 this, 这个 this 本身就是 A *const this
的, 你这么一弄, 就会变成 const A* const this
了, 也就是说, this指向内容是不可改变的了, 只能调用不能修改.
可能就会有人疑惑, 这有啥用啊?
- 很显然, 防止程序员写错了, 并且对于调用来说适用范围更广泛.
- const对象也可以进行调用对应const函数
class Date {
private:int _day;int _month;int _year;public:Date(int year = 1, int month = 1, int day = 1);void DatePrint() const;bool operator<(const Date& d);bool operator<=(const Date& d);bool operator>(const Date& d);bool operator>=(const Date& d);bool operator==(const Date& d);bool operator!=(const Date& d);Date& operator+=(int day);Date operator+(int day);Date& operator++(); // 前置++Date operator++(int); // 后置++
};void Date::DatePrint() const {cout << "year:" << this->_year << " ";cout << "month:" << this->_month << " ";cout << "day:" << this->_day << endl;
}
权限的放大与缩小???
先看下面问题:
所有函数都需要加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成员函数吗?
答: 不可以, 调用不了, 因为这是一种权限扩大, this指针是不能改指向, 也不能修改指向的内容, 但是这个非const函数是允许修改的, 权限扩大嘛
非const对象可以调用const成员函数吗?
可以, 如果没有非 const 函数的话, 编译器则去退而求其次, 去调用 const 成员函数.
const成员函数内可以调用其它的非const成员函数吗?
不可以。const
成员函数承诺不会修改调用该函数的对象的状态。而非const
成员函数可能会修改对象的状态。如果在const
成员函数内调用非const
成员函数,就可能违背const
成员函数不修改对象状态的承诺,因此编译器会报错。
非const成员函数内可以调用其它的const成员函数吗?
可以。非const
成员函数本身就允许修改对象的状态,而const
成员函数不会修改对象的状态,所以在非const
成员函数内调用const
成员函数不会违背任何规则。
2.8. 取地址操作符重载
概念: 用于返回对象地址的特殊重载取地址成员函数,不写编译器会自动生成。
注:编译器会自动生成两个:一个是带const的取地址操作符重载,另一个是不带const的取地址操作符重载
class A
{
public:int a = 10;A* operator&(){return this;}const A* operator&() const{return this;}
};void test6()
{A aa1;A aa2;cout << (&aa1) << endl; // 000000B3E67BFA54cout << (&aa2) << endl; // 000000B3E67BFA74
}
调用规则:一般而言,const变量会去调用const版的取地址函数,非const变量会去调用非const版的取地址函数
其他用途:返回空/假地址。
3. 初始化列表
CPP的构造函数包括两部分,一是我们上面说到的函数体部分,另一部分是初始化列表部分。
初始化列表:CPP为解决所有成员变量初始化问题而对构造函数做的补充。
初始化列表的意义
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 实际上,有些成员变量只有函数体构造函数是无法对其进行初始化操作的。类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员且该类没有默认构造函数时
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
- 初始化列表的变量括号后面可以写一句语法语句.
- 单参数构造函数支持类型转换(隐式类型转换)
- 多参数构造函数支持隐式类型转换
4. Static成员
static: 静态成员变量,存储于静态区中
int A::_scount = 0;class A
{
public:A() { ++_scount; }A(const A& t) { ++_scount; }~A() { --_scount; }static int GetACount() { return _scount; }
private:static int _scount;
};void TestA()
{cout << A::GetACount() << endl; // 输出初始计数 0A a1, a2; // 调用2次构造函数,计数+2A a3(a1); // 调用拷贝构造函数,计数+1cout << A::GetACount() << endl; // 输出当前计数 3
}
// 函数结束时3个对象析构,计数归零
特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用类名:静态成员或者对象, 静态成员来访问.
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public,proted,private访问限定符的限制
拓展: C++中static
关键字用法总表(含声明/定义分离情况)
类别 | 声明位置 | 定义位置 | 正确写法示例 | 错误写法示例 | 关键规则 |
成员函数 | |||||
static成员函数 | 类内声明 | 类内定义 |
| - | 声明和定义均需 |
类内声明 | 类外定义 |
|
❌ | 定义禁止加 | |
非static成员函数 | 类内声明 | 类内定义 |
| - | 始终不加 |
类内声明 | 类外定义 |
| - | ||
成员变量 | |||||
static成员变量 | 类内声明 | 类外定义 |
|
❌ | 定义禁止加 |
非static成员变量 | 类内声明 | (无定义步骤) |
| - | 随对象创建/销毁 |
全局函数 | 文件内声明 | 同文件或头文件 |
| - | 声明和定义均需 (文件作用域) |
全局变量 | 文件内声明 | 同文件 |
|
❌ | 声明和定义均需 (文件作用域) |
局部变量 | 函数内 | (无分离概念) |
| - | 首次初始化,程序生命周期 |
5. 友元函数/类
- 友元函数: 允许类外函数访问类内成员的特殊函数类型. (为一些类外函数访问私有成员变量"走后门". )
- 友元函数的特性:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰,因为没有this指针
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
- 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。
比如上述time类和date类,在time类中声明date类为其友元类,那么可以在date类中直接访问time类的私有成员变量,但想在time类中访问date类中私有的成员变量则不行。 - 友元关系不能传递
如果c是b的友元,b是a的友元,则不能说明c时a的友元。 - 友元关系不能继承,在继承位置再给大家详细介绍。
- 友元关系是单向的,不具有交换性。
class Time
{friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类
中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}private:int _hour;int _minute;int _second;
};
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1): _year(year), _month(month), _day(day){}void SetTimeOfDate(int hour, int minute, int second){// 直接访问时间类私有的成员变量_t._hour = hour;_t._minute = minute;_t._second = second;}private:int _year;int _month;int _day;
}
6. 内部类
- 概念:如果一个类定义(typedef)在另一个类的内部,这个内部类就叫做内部类。
- 特性
- b只是受到a类类域的限制和访问限定符号的限制
sizeof(a);这里并不包含b的大小 - 内部类天生是外部类的友元
- b只是受到a类类域的限制和访问限定符号的限制