前言:看懂这篇文章你需要知道 C语言的所有知识 C++引用 函数重载的概念 缺省值的概念
(后俩不懂的自己搜一下,一分钟就学会了)
为了便于理解与举例,本文的代码大多都是代码段,不是完整的程序
目录
一、类和对象基础
1.1 类的基本概念
知识要点
1.2 对象实例化
内存分配原理
1.3 this指针
为什么要有this指针
this指针特点
备注
1.4 const成员函数
二、类的默认成员函数
2.1 构造函数
构造函数特点
默认构造
编译器自动生成的构造函数
初始化列表(很重要)
1.用法
2.特点
3.初始化顺序
2.2 析构函数
析构函数特点
2.3 拷贝构造
知识要点
深拷贝
1. 浅拷贝(Shallow Copy)
2. 深拷贝(Deep Copy)
2.4 赋值运算符重载
2.4.1运算符重载
基础知识
重载运算符函数作为成员函数
前置后置自增/减运算符的重载
<<和>>的重载
2.4.2 赋值运算符重载
2.5 默认成员函数补充
三、高级特性
3.1 static成员
(1)静态成员变量
(2)静态成员函数
(3)小练习
3.2 友元
友元的类型
友元的重要特性
友元的优缺点
3.3 内部类
注意点
3.4 匿名对象
3.5 类型转换
四、总结
一、类和对象基础
1.1 类的基本概念
类是C++面向对象编程的核心概念,它将数据(成员变量)和操作数据的方法(成员函数)封装在一起。类定义的基本格式如下:
class Date {
public:// 公有成员(外部可访问)void Print();private:// 私有成员(仅类内部可访问)int _year;int _month;int _day;
};
知识要点
-
class关键字定义类,Date是类名,{}中为类的主体,类体中内容称为类的成员
-
类成员包括成员变量和成员函数
* 类中的变量称为类的属性或成员变量(_year等,_ 是为了区分);
* 类中的函数称为类的⽅法或者成员函数(Print()),卸载类里的成员函数默认为inline
-
访问限定符控制封装性
* public修饰的成员在类外可以直接被访问
* protected和private修饰的成员在类外不能直接被访问
* 访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌
* class定义成员没有被访问限定符修饰时默认为private
-
类定义结束必须有分号
-
类域
类定义了⼀个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。
//class外部写时 void Date::Print(){cout << _year << "-" << _month << "-" << _day << endl;}
-
struct也可以定义类, struct默认为public,一般不用
1.2 对象实例化
类只是蓝图,必须实例化为对象才能使用(其实和定义变量区别不大):
Date d1; // 实例化对象
d1.Print(); // 调用成员函数
内存分配原理
-
对象只存储成员变量,不存储成员函数(函数代码在代码段共享)
-
空类对象占1字节(标识对象存在)
-
对象大小遵循内存对齐规则
1.3 this指针
编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this 指针
class Date
{public:// void Init(Date* const this, int year, int month, int day)void Init(int year, int month, int day){this->_year = year;_month = month;//有无this都行,因为成员变量的名字与普通变量不一样this->_day = day;}//void Print(Date* const this)void Print(){cout << _year << "/" << _month << "/" << _day << endl;}private:int _year;int _month;int _day;
};
int main()
{// Date类实例化出对象d1和d2 Date d1;Date d2;// Init(&d1, 2024, 3, 31);d1.Init(2024, 3, 31);// Print(&d1);d1.Print();d2.Init(2024, 7, 5);d2.Print();return 0;
}
为什么要有this指针
Date类中有Init与Print两个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init和 Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?this指针明确标识了当前调用成员函数的对象实例。
- 真实原型为Init(&d1, 2024, 3, 31); 显示表示成 d1.Init(2024, 3, 31);
- 真实原型为Print(&d1);显示表示成d1.Print();
- 真实原型为void Init(Date* const this, int year, int month, int day);显示表示成void Init(int year, int month, int day);
this指针特点
-
编译器自动添加为成员函数的第一个参数
-
类型为Date
* const
(常量指针),证明指针不能修改。比如this = nullptr就是错误的。 -
类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值, this- >_year = year;
-
不能在形参/实参位置显式写出,但可在函数体内使用
-
this指针一个重要作用是支持链式调用
#include<iostream> class A { public:A& Add(int x){value += x;return *this; // 返回当前对象}void print(){printf("%d", value);} private:int value = 0; };int main() {A cal;cal.Add(1);cal.Add(2);cal.Add(3); // 链式调用cal.print(); }//输出6
- this指针作为函数调用的参数(虽然是隐式传递),其生命周期与函数调用一致,存储在栈帧中。
备注
this指针基本不会显示表示,它也不能随意修改,放的位置也只能是参数第一个,所以以后尽量不要想通过this指针的传参方式来改变你代码中不合适的逻辑与地方。
1.4 const成员函数
用const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面
class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}// void Print(const Date* const this) const void Print() const{cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;d1.Init(2024, 3, 31);d1.Print();return 0;
}
const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进⾏修改。 const修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this
二、类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数(C++11前),需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。
2.1 构造函数
构造函数在对象创建时自动调用,用于初始化,相当于Init,主要任务不是开创空间,但如果是Stack这样需要自主分配内存的类,在构造函数中也可以开。
class Date {
public:// 构造函数Date(int year = 1, int month = 1, int day = 1) {_year = year;_month = month;_day = day;}
private:int _year, _month, _day;
};
typedef int STDataType;
class Stack
{public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};
这里的 Date(int year = 1, int month = 1, int day = 1) ; Stack(int n = 4) ;都叫构造函数
构造函数特点
-
函数名与类名相同
-
无返回值(连void都不写)
-
可以重载
默认构造
- 无参构造函数、全缺省构造函数和编译器生成的构造统称"默认构造"。
// 1.无参构造函数
Date()
{_year = 1;_month = 1;_day = 1;
}// 2.全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{_year = year;_month = month;_day = day;
}// 3.编译器默认⽣成的构造函数
//......(啥都不用写)//调用
Data.d1;
# 这三个函数有且只有⼀个存在,不能同时存在
# 如果用的是⽆参构造函数、编译器生成的构造,或者调用全缺省构造函数时不想传参,那么调用时直接写成 Data d1;即可,后面不要加括号,否则系统会认定写的是函数声明。其实也就是在实例化的同时完成构造.
总结⼀下就是不传实参就可以调用的构造就叫默认构造
- 带参构造,半缺省构造只能叫构造函数。
// 4.带参构造函数 Date(int year, int month, int day){_year = year;_month = month;_day = day;}// 5.半缺省构造函数Date(int year, int month, int day=27){_year = year;_month = month;_day = day;}
# 因为这个这个函数重载,所以 1和4/5 可以同时存在
# 这两个不传参无法使用
编译器自动生成的构造函数
- 一般还是要自己写,编译器自动生成的不保险
- 如果类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数,⼀旦用户显式定义编译器将不再生成。我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。
- 对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决。
初始化列表(很重要)
我们其实一般用初始化列表来写构造的情况居多,因为其效率高于函数体内赋值,也比较方便
1.用法
构造函数初始化还有⼀种⽅式,就是初始化列表,初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成 员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。每个成员变量在初始化列表中只能出现⼀次。
class Date {
public:Date(int year) : _year(year) , _month(1) , _day(1) {}
};
2.特点
- 引⽤成员变量,const成员变量,没有默认构造的类类型变量,这三种必须放在初始化列表位置进⾏初始化,否则会编译报错。
#include<iostream> using namespace std; class Time { public:Time(int hour):_hour(hour){cout << "Time()" << endl;}private:int _hour; }; class Date { public:Date(int& x, int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day), _t(12), _ref(x), _n(1){}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;Time _t; // 没有默认构造 int& _ref; // 引用 const int _n; // const };int main() {int i = 0;Date d1(i);d1.Print();return 0; }
time就叫上面说的自定义类型,会调⽤这个成员变量的默认构造函数初始化
如果把const和引用放在函数体内或time没有构造函数,就会出现
- C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的。
private:// 注意这⾥不是初始化,这⾥给的是缺省值,这个缺省值是给初始化列表的 // 如果初始化列表没有显⽰初始化,默认就会⽤这个缺省值初始化 int _year = 1;int _month = 1;int _day;Time _t = 1;const int _n = 1;int* _ptr = (int*)malloc(12);
- 初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致。
先初始化a2,此时a1还没有被赋成a=1,所以是随机值。
3.初始化顺序
注 :不要和构造函数的传参搞混,初始化列表初始的是成员变量,而传参传的是普通变量。
2.2 析构函数
析构函数在对象销毁时自动调用,用于资源清理,这里我们用Stack举例,因为Date没有资源需要释放,所以说Date是不需要析构函数的。即有资源占用,才用析构。
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};
析构函数特点
-
类名前加~
-
无参数无返回值
-
一个类只能有一个析构函数
-
内置类型成员不处理,自定义类型成员调用其析构
-
⼀个局部域的多个对象,C++规定后定义的先析构。
2.3 拷贝构造
拷贝构造用同类对象初始化新对象,所以拷⻉构造是⼀个特殊的构造函数
class Date
{
public:Date(int year = 1, 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;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};void Func1(Date d)
{cout << &d << endl;d.Print();
}
Date Func2()
{Date tmp(2024, 7, 5);tmp.Print();return tmp;
}
知识要点
- 拷⻉构造函数是构造函数的⼀个重载。
- 拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤,有多个参数时,后⾯的参数必须有缺省值(使⽤传值⽅式编译器直接报错,因为调用拷贝构造函数要拷贝实参,这时又需要调用拷贝构造函数,会引发⽆穷递归调⽤,如下图)
-
常见的有三种需要拷贝构造的情况:
int main() {Date d1(2024, 7, 5);Date d3(d1); //第1)种Date d4 = d1; //也能这么写Func1(d1); //第2)种Date ret = Func2();//第3)种ret.Print(); }
1)使用同类型的对象去初始化另一个对象
2)将一个对象作为实参传递给一个非引用类型的形参,
但如果使用引用传参可以不用拷贝,提高效率,即:
void Func1(Date &d)
{cout << &d << endl;d.Print();
}//调用
Func1(d1); //第2)种
3)从一个返回类型为非引用类型的函数返回一个对象
但如果使用传引用返回可以不用拷贝,提高效率,用这个无法演示,因为tmp是临时对象,函数结束就销毁,不能再次引用。后面会有相关演示。
-
另一种拷贝方式
Date(Date* d)
{_year = d->_year;_month = d->_month;_day = d->_day;
}//调用
Date d1(2024, 7, 5);
Date d2(&d1);;
d2.Print();
这⾥可以完成拷⻉,但是不是拷⻉构造,只是⼀个普通的构造
深拷贝
1. 浅拷贝(Shallow Copy)
-
定义:仅复制对象的成员值(包括指针的值),不复制指针指向的资源
-
行为:新旧对象的指针成员指向同一块内存
-
编译器默认行为:当类未显式定义拷贝构造函数或赋值运算符时,编译器生成的默认版本执行浅拷贝
2. 深拷贝(Deep Copy)
-
定义:不仅复制对象成员值,还为指针成员分配新内存并复制内容
-
行为:新旧对象的指针成员指向不同的内存块,但内容相同
-
实现方式:需要自定义拷贝构造函数和赋值运算符
我们上面所讲的都是浅拷贝,像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以其实我们不需要写。但像 Stack 这样有明显的资源指向的类,普通的值拷贝并不符合要求,它需要通过拷贝开辟另一块空间,所以要我们自己实现深拷贝。
下面给一个深拷贝的例子:
这段代码最好仔细理解,有一些上面讲到但没举例子的知识点
#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申请空间失败");return;}_capacity = n;_top = 0;}Stack(const Stack& st){// 需要对_a指向资源创建同样大的资源再拷贝值 _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc申请空间失败!!!");return;}memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}void Push(STDataType x){if (_top == _capacity){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};// 两个Stack实现队列
class MyQueue
{
public:
private:Stack pushst;Stack popst;
};int main()
{Stack st1;st1.Push(1);st1.Push(2);// Stack不显示实现拷贝构造,用自动生成的拷贝构造完成浅拷贝 // 会导致st1和st2里面的_a指针指向同一块资源,析构时会析构两次,程序崩溃 Stack st2 = st1;MyQueue mq1;// MyQueue自动生成的拷贝构造,会自动调用Stack拷贝构造完成pushst/popst // 的拷贝,只要Stack拷贝构造自己实现了深拷贝,他就没问题 MyQueue mq2 = mq1;return 0;
}
2.4 赋值运算符重载
2.4.1运算符重载
运算符重载是具有特殊名字的函数,简单说就是赋予普通运算符新的含义,C++规定类类型对象使⽤运算符时,必须转换成调⽤对应的运算符重载,若没有对应的运算符重载,则会编译报错。
基础知识
用法:和其他普通函数一样
返回类型 函数名(参数列表)
{函数体}
- 函数名由operator和后⾯要定义的运算符共同构成
- 不能重载的运算符:.* :: sizeof ?: . 本身就没有的运算符(@等)(常考!!)
C++ 规定,运算符重载至少需要一个类类型(用户自定义类型)的参数,不能完全重载仅作用于内置类型(如
int
、float
、char
等)的运算符。如: int operator+(int a, int b) 或者 int operator+(int a, char b) 等
bool operator==(const Date& d1, const Date& d2)
{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}int main()
{Date d1(2024, 7, 5);Date d2(2024, 7, 6);// 第一种,运算符重载函数可以显示调用 if (d1.operator==(d2))cout << "ture";// 第二种,编译器会转换成 d1.operator==(d2); if(d1 == d2)cout << "ture";return 0;
}
- 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
- 重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。
重载运算符函数作为成员函数
那么问题来了,重载为全局的函数怎么能访问私有成员(_year等)呢,那肯定不能,如果你在日期类外边直接补上上面的代码,你肯定是编译不过的。下面有四种解决办法:
1、成员放公有(直接去掉private)
2、Date提供getxxx函数
class Date { private:int year;int month;int day; public:// 构造函数Date(int y, int m, int d) : year(y), month(m), day(d) {}// 提供getter函数int getYear() const { return year; }int getMonth() const { return month; }int getDay() const { return day; } }; bool operator==(const Date& d1, const Date& d2) {return d1.getYear() == d2.getYear()&& d1.getMonth() == d2.getMonth()&& d1.getDay() == d2.getDay(); }
3、友元函数(看后文)
4、重载为成员函数
一般我们推荐第四个
- 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。
class Date { public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}//bool operator==(Date* const this,const Date& d)bool operator==(const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;} private:int _year;int _month;int _day; };
前置后置自增/减运算符的重载
- C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。
class Date { public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}Date& operator++(){cout << "前置++" << endl;//...return *this;}Date operator++(int)//就写个int就行{Date tmp;cout << "后置++" << endl;//...return tmp;}private:int _year;int _month;int _day; };int main() {Date d1(2024, 7, 5);Date d2(2024, 7, 6);// 编译器会转换成 d1.operator++(); ++d1;// 编译器会转换成 d1.operator++(0); d1++;return 0; }
我们可以发现,后置++需要进行拷贝构造,而前置不需要,所以前置比后置高效一点
<<和>>的重载
你知道为什么用cin,cout可以不用写打印格式吗,其实它的本质也是运算符重载,感兴趣可以去搜一下。那我们想输入、打印日期类,也可以利用重载。讲之前首先要知道cin,cout属于istream&与ostream&类型。
- 如果写成成员函数
#include<iostream> using namespace std; class Date { public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void operator<<(ostream& out){out << _year << "年" << _month << "月" << _day << "日" << endl;}void operator>>(istream& in){cout << "请依次输入年月日:>";in >> _year >> _month >> _day;}private:int _year;int _month;int _day; };int main() {Date d1;// 输入d1 >> cin;Date d2(2025, 5, 28);// 输出d2 << cout;d1 << cout;return 0; }
由于第一个参数必须是this指针,所以只能写成 d1 >> cin; 倒反天罡!而且这个函数不能连续打印,只能一个一个来。所以我们写成这样并不好。
那就写成全局函数,那第一个参数就自由了,我们为了访问私有,可以把它设为友元函数(在后文)。另外,我们为了可以连续打印,可以把返回值改成cout与cin类型的。
代码如下:
#include<iostream> using namespace std; class Date {// 友元函数声明 friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day; };ostream& operator<<(ostream& out, const Date& d) {out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out; }istream& operator>>(istream& in, Date& d) {cout << "请依次输入年月日:>";in >> d._year >> d._month >> d._day;return in; }int main() {Date d1;Date d2(2025, 5, 28);cin >> d1; //operator>>(cin, d1)cout << d1 << d2;return 0; }
2.4.2 赋值运算符重载
上面说的是C++的一个语法,现在说默认成员函数的第四个:赋值运算符重载。
赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象。
Date& operator=(const Date& d)
{ if (this != &d){_year = d._year;_month = d._month;_day = d._day;}// d1 = d2表达式的返回对象应该为d1,也就是*this return *this; //正好对应上面我们没举例子的可以不用拷贝构造的第3)种情况
}int main()
{Date d1(2024, 7, 5);Date d2(d1);Date d3(2024, 7, 6);d1 = d3; //赋值重载Date d4 = d1;//拷贝构造return 0;
}
与上面的拷贝构造类似,像 Date 这样没有指向什么资源的类,编译器自动生成的赋值运算符重载就可以完成我们所需要的拷贝,所以不需要我们显示的实现,上面是为了大家理解。但像 Stack这样的类就不行。
2.5 默认成员函数补充
- 如果⼀个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载和拷⻉构造,否则就不需要。
- 如果一个类中有自定义成员类型,那么它就会自动调用该自定义类型的(默认)成员函数 ,如果调用不到就报错。
- 如果需要拷贝成员是数组,默认的拷贝构造函数会逐元素地拷贝一个数组类型的成员。
- ⾮const对象也可以调⽤const成员函数,这是⼀种权限的缩⼩
- 还有两个默认成员函数:普通取地址运算符重载、const取地址运算符重载。⼀般这两个函数编译器⾃动 ⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。感兴趣的可以去搜一搜。
三、高级特性
3.1 static成员
⽤static修饰的成员变量,称之为静态成员变量,生命周期是全局,可以理解成,static定义的变量不易销毁,能一直用,先看代码:
#include<iostream>
using namespace std;
class A
{
public:static int p;A(){++_scount;}A(const A& t){++_scount;}~A(){--_scount;}static int GetACount(){return _scount;}private:// 类里面声明 static int _scount;
};// 类外面初始化
int A::_scount = 0;
int A::p = 2025;
(1)静态成员变量
-
静态成员变量⼀定要在类外进⾏初始化,静态成员变量不能在声明位置给缺省值初始化,不⾛构造函数初始化列表
-
静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区
-
静态成员也是类的成员,受public、protected、private访问限定符的限制。
(2)静态成员函数
-
⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针,所以静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的;而⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
-
突破类域就可以访问静态成员,可以通过 类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。比如上面代码的:
cout << A::GetACount() << endl;
cout << a1.GetACount() << endl;
(3)小练习
第一题
答案:B
提示:
全局对象的构造顺序是不确定的(跨编译单元),但析构顺序是构造的逆序。
局部对象(非静态)在作用域结束时析构,顺序是构造的逆序。
静态局部对象在程序结束时析构,顺序是构造的逆序。
动态分配的对象(通过
new
创建)在delete
时析构。
第二题 求1+2+3+...+n_⽜客题霸_⽜客⽹https://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=3&rp=3&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking
3.2 友元
友元(friend)是C++提供的一种特殊机制,它允许一个外部函数或类访问另一个类的私有和保护成员。友元关系通过在类内部声明来建立,使用friend关键字标识。
虽然这段代码看这长,但读起来巨简单
#include <iostream>
using namespace std;// 前置声明,因为B在A后,防止A的友元函数声明时,编译器不认识B
class B;class A
{// 友元函数声明friend void f1(const A& aa, const B& bb);// 友元类声明friend class f2;private:int _a1 = 1;int _a2 = 2;public:void display() {cout << "A类公有方法: " << _a1 << ", " << _a2 << endl;}
};// 类B定义
class B
{// 同一个函数可以成为多个类的友元friend void f1(const A& aa, const B& bb);private:int _b1 = 3;int _b2 = 4;public:void display() {cout << "B类公有方法: " << _b1 << ", " << _b2 << endl;}
};// 友元类定义
class f2
{
public:void showA(const A& aa) {cout << "友元类访问A的私有成员: " << aa._a1 << ", " << aa._a2 << endl;}void modifyA(A& aa, int x, int y) {aa._a1 = x;aa._a2 = y;cout << "友元类修改A的私有成员后: " << aa._a1 << ", " << aa._a2 << endl;}
};// 友元函数实现
void f1(const A& aa, const B& bb)
{cout << "友元函数访问A和B的私有成员:" << endl;cout << "A._a1 = " << aa._a1 << ", A._a2 = " << aa._a2 << endl;cout << "B._b1 = " << bb._b1 << ", B._b2 = " << bb._b2 << endl;
}int main()
{A a;B b;f2 fc;// 正常访问公有方法a.display();b.display();// 使用友元函数f1(a, b);// 使用友元类fc.showA(a);fc.modifyA(a, 10, 20);a.display();return 0;
}
-
友元的类型
1.友元类:整个类被声明为另一个类的友元,该类的所有成员函数都可以访问友元类的私有和保护成员。
2.友元函数:一个独立的函数,被声明为某个类的友元后,可以访问该类的所有成员。
-
友元的重要特性
单向性:友元关系是单向的。如果A是B的友元,并不意味着B也是A的友元。
非传递性:友元关系不能传递。如果A是B的友元,B是C的友元,并不意味着A是C的友元。
不受访问限定符限制:友元声明可以放在类的任何位置(public、protected或private),效果相同。
不是成员函数:友元函数不是类的成员函数,它只是被授予了访问权限的普通函数。
-
友元的优缺点
有元提供了便利,但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。
3.3 内部类
其实就是在类里面再定义一个类,里边的这个类就叫内部类,内部类默认是外部类的友元类
#include<iostream>
using namespace std;
class A
{
private:static int _k;int _h = 1;public:class B // B默认就是A的友元 {public:void foo(const A& a){cout << _k << endl; cout << a._h << endl; //访问外部私有}int _b1;};
};int A::_k = 1;int main()
{cout << sizeof(A) << endl; //运行结果为4,说明外部类定义的对象中不包含内部类A::B b;A aa;b.foo(aa); //1 1return 0;
}
注意点
- 内部类是⼀个独⽴的类,跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
- 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其 他地⽅都⽤不了。
3.4 匿名对象
- ⽤ 类型 (实参) A()或A(1) 定义出来的对象叫做匿名对象,相⽐之前我们定义的 类型 对象名(实参) A aa1或A aa1(1) 定义出来的叫有名对象
- 匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
用sort进行降序排列://用有名对象
greater<int> gt;
sort(a, a + 8, gt);//用匿名对象
sort(a, a + 8, greater<int>());
有些时候用它是很方便的,如果看不懂上面的代码就先了解即可
3.5 类型转换
- C++允许内置类型隐式转换为类类型对象,前提是类提供了以该内置类型为参数的构造函数。(类类型之间也可以进行转换,前提是有适当的构造函数或转换函数。)
- 如果我们不希望自动发生隐式转换,可以使用
explicit
关键字修饰构造函数。
#include <iostream>
using namespace std;class Distance
{
public:// 不支持隐式类型转换 // explicit Distance(int meters)// 允许int到Distance的隐式转换Distance(int meters) : m_meters(meters) {cout << "Distance构造函数: " << m_meters << "米" << endl;}void display() const {cout << "距离: " << m_meters << "米" << endl;}private:int m_meters;
};void Print(const Distance& d)
{d.display();
}int main()
{Distance d1 = 100; // 隐式转换:int -> DistanceDistance d2(500); //调用构造函数Print(200); // 隐式转换:int -> Distancereturn 0;
}
- C++11支持使用花括号初始化列表进行多参数转换。
Distance (int a,int b) : m_meters(a) , n_meters(b){cout << "Distance构造函数: " << m_meters << "米" << endl;}Distance d2 = {5 , 3}; // 隐式转换:int -> Distance
四、总结
⾯向对象三⼤特性:封装、继承、多态。我们学习了类与对象的基本知识,默认成员函数及一些其他的高级特性,主要体现出来面向对象语言的封装特性,这是基础,一定要搞清楚。可以自己模拟实现一下日期类。
这是日期类的完整代码,大家可以参考 日期类实现
后记:如果你能看完,你就学完了类与对象90%的常见知识,剩下的需要我们在实践中探索,加油!如果对你有帮助,点个红心支持一下吧!