
目录
今天我们学习C++类和对象的最后一些知识,主要是为了更加深入地理解和使用类和对象。
1. 再谈构造函数
1.1. 构造函数体赋值
我们之前将到过,类会默认一个构造函数。然后在实例化对象时,编译器就会调用这个构造函数,给成员变量赋值。
class Date { public: Date(int year = 1 , int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: //声明 int _year; int _month; int _day; }; int main() { //对象 Date d1(2025, 8, 23); Date d2;//未定义无参的默认构造 写成全缺省就ok return 0; }如果没有定义无参的构造函数,我们建议将构造函数写成全缺省的,因为这样不管是有参对象还是无参对象都能够使用这个构造函数。
我们来看一下如果不是全缺省的情况:
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1(2025,8,23); //对于下面这个无参的对象,无法调用构造函数 //因为我们只有含参的构造函数 Date d2; }
上面是通过构造函数进行初始化,除此之外,C++还提供了一种通过初始化列表的方式。
1.2. 初始化列表
初始化列表:以一个冒号开始 ,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
我们试着使用初始化列表的形式,初始化我们的Date日期类:
class Date { public: //初始化列表 Date(int year = 1, int month = 1, int day = 1) :_year(year) ,_month(month) ,_day(day) { } private: //声明 int _year; int _month; int _day; }; int main() { //对象 Date d1(2025, 8, 23); Date d2; return 0; }
1.2.1. 初始化列表的价值
从上面的代码来看,好像和使用构造函数体赋值并没有优化什么,那么为什么还要使用初始化列表呢?
这里我们就要回忆起一个知识点,有些成员变量是需要在声明时就进行初始化的!!!
因为如果需要在变量声明时就初始化的话,构造函数体赋值的方式,明显是会报错的。因为在没有调用构造函数之前,都无法进行初始化。而初始化列表就能完美解决这个问题!
Q:思考,那些成员是必须在定义的地方就进行初始化呢?
A:(a) 引用成员变量 (b)const成员变量 (c)自定义类型(无默认构造函数)
PS:这里补充说明一下为什么自定义类型(无默认构造函数)必须在定义时初始化。先要注意的是,这里并不是说所有的自定义类型,而是特指那些没有默认构造函数的自定义类型,而默认的构造函数是无参构造函数。所以在没有无参构造函数的情况下,如果不提供初始化的参数(或者说是一个无参的对象),是肯定会报错的。
class A { public: A(int a) :_a(a) { } private: int _a; }; class Date { public: //只能在初始化列表初始化 Date(int year, int n, int ref,int a) :_n(n) ,_ref(ref) //这里的_aa并没有别的意思,只是已经有了_a,避免重名 ,_aa(a) { _year = year; } private: //可以在定义时初始化;也可以定义时不初始化,后面再赋值修改 int _year; //只能在定义的时候初始化 const int _n; int& _ref; A _aa; };总结:
- 每个成员变量在初始化列表中只能出现一次(每个变量只能被初始化一次)
- 必须要使用初始化列表的成员变量
引用成员变量
const 成员变量
自定义类型成员 ( 该类没有默认构造函数 )
尽量使用初始化列表的方式,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化(可以理解为,与其让编译器去控制初始的过程,不如自己主动控制初始化)
成员变量在类中的声明次序就是在初始化列表中的初始化次序,与其在初始化列表中的先后次序无关
class OrderMatters { private: int a; int b; public: // 虽然写的是 : b(1), a(2) // 但实际初始化顺序是:先 a(2),再 b(1)(按声明顺序) OrderMatters() : b(1), a(2) {} };
1.3. explicit关键字
我们先来看一个奇妙的事情:
我定义的d2很明显是一个Date日期类,但是2025很明显是一个int类型,但是我却成功运行了这段程序,并且发现2025被初始化成了_year。这是怎么回事?
实际上,单参数构造函数(Single-argument constructor)可以用于隐式类型转换。这是C++中一个强大但需要小心使用的特性。
但是这样的话可能会出现一些不合法数据被错误的传入了构造函数,而explicit关键字就是用来限制这种类型转换的:
class Date { public: explicit Date(int year=1, int month=1, int day=1) : _year(year) , _month(month) , _day(day) { } private: int _year; int _month; int _day; };
此时这个错误就会被检测出来,所以说explicit关键字就是用来禁止单参构造函数的隐式转换
2. static成员
2.1. 问题引入
我们通过一个问题来进入static的探讨,Q:统计类型A被创造了多少次?
//全局变量 -- 不提倡 int num = 0; class A { }; A Func(A a) { A copy(a); return copy; } int main() { A a1; A a2 = Func(a1); cout << num << endl; }如果我们直接判断的话会比较不方便,找起来也比较麻烦。
方法一:全局变量
这里可以直接将构造函数和拷贝构造函数调用的记录打印不就好了,这里我们分别使用不同的两个全局变量来记录:
int num1 = 0; int num2 = 0; class A { public: A() { ++num1; } A(const A& aa) { ++num2; } }; A Func(A a) { A copy(a); return copy; } int main() { A a1; A a2 = Func(a1); cout << num1 << endl; cout << num2 << endl; }
这样确实可以得到正确的答案,但是会出现一个问题:在项目中,如果将头文件中定义全局变量的话可能发生链接错误;并且全局变量也破坏了封装性。
方法二:静态成员变量
#include <iostream> using namespace std; class A { public: A() { ++_count1; } A(const A& aa) { ++_count2; } //静态成员变量属于整个类 static int _count1;//声明 static int _count2; int _a; }; A Func(A a) { A copy(a); return copy; } //定义 int A::_count1 = 0; int A::_count2 = 0; int main() { A a1; A a2 = Func(a1); cout << sizeof(a1) << endl; cout << a1._count1 << endl; cout << a2._count1 << endl; cout << a1._count2 << endl; cout << a2._count2 << endl; cout << A::_count1 << endl; cout << A::_count1 << endl; return 0; }
PS:这里可能在不同的计算机上会有不同的答案,可能是4113311(因为编译器的优化程度不同)
这里的结果中的 4 比较好理解,因为整个类中有一个int型的成员变量,_count1、_count2都是静态成员变量(被储存在静态区)。
这里还要注意一个点:就是静态成员变量在类中声明,不能在类中直接定义!!!定义时不添加static
如果静态成员变量是private私有的,需要编写访问接口:
class A { public: A() { ++_count1; } A(const A& aa) { ++_count2; } //访问接口 int GetCount1() { return _count1; } int GetCount2() { return _count2; } private: //静态成员变量属于整个类 static int _count1;//声明 static int _count2; int _a; }; A Func(A a) { A copy(a); return copy; } //定义 int A::_count1 = 0; int A::_count2 = 0; int main() { A a1; A a2 = Func(a1); cout << a1.GetCount1() << endl; cout << a2.GetCount1() << endl; cout << a1.GetCount2() << endl; cout << a2.GetCount2() << endl; return 0; }
如果我并没有实例化对象,但是我仍想访问这个静态成员变量:
正常情况下是会报错的,但是我们可以将这个访问接口也写成静态成员函数:
class A { public: A() { ++_count1; } A(const A& aa) { ++_count2; } //静态访问接口 static int GetCount1() { return _count1; } static int GetCount2() { return _count2; } private: //静态成员变量属于整个类 static int _count1;//声明 static int _count2; int _a; }; A Func(A a) { A copy(a); return copy; } //定义 int A::_count1 = 0; int A::_count2 = 0; int main() { A a1; A a2 = Func(a1); cout << a1.GetCount1() << endl; cout << a2.GetCount2() << endl; cout << A::GetCount1(); return 0; }
2.2. 特性
- 静态成员为所有类对象共享,不属于某一个类的实例
- 静态变量在类中声明,在类外定义
- 类静态成员可以使用,类名::静态成员、对象.静态成员来访问
- 类静态成员和普通成员一样,有public、private、protected修饰
- 静态成员函数没有this指针,所以不能访问非静态成员变量(因为非静态变量与对象肯定是对应的)
3. C++11成员初始化新玩法
C++11支持非静态成员变量在类中生命时,直接进行初始化赋值,注意这里的初始化赋值本质上是一种缺省值的形式。
class B { public: B(int b = 0) :_b(b) { } private: int _b; }; class A { public: private: //非静态成员变量,可以在成员声明时给缺省值 int a = 10; B b = 20;//隐士类型转换 int* p = (int*)malloc(4); };
4. 友元
友元包括友元函数和友元类。
友元突破封装性性,确实在一定程度上有灵活性。但是,友元同样增加了代码耦合性,所以友元的使用要有限制。
4.1. 友元函数
友元函数可以访问类中私有、受保护的成员变量,它是定义在类之外的普通函数,并不属于某一个类,只是需要在类中声明,声明时需要加上friend关键字:
class Date { //友元函数 friend ostream& operator<<(ostream& out, const Date& d); public: private: int _year; int _month; int _day; }; ostream& operator<<(ostream& out, const Date& d) { out << d._year << "-" << d._month << "-" << d._day << endl; return out; }总结:
- 友元函数可以访问类中的私有、受保护的成员变量,但是本身不是成员函数
- 一个函数可以是多个类的友元函数
- 友元函数的使用和普通函数原理一样
- 友元函数不能使用const修饰(换句话说,声明友元函数时在后面加上const,语法上就是错误的)
- 友元函数不受访问修饰符的限制
4.2. 友元类
友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类的非公有成员。
//友元 class Date;//Date类的声明 class Time { //友元类 friend class Date; 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 { 友元函数 friend ostream& operator<<(ostream& out, const Date& d); public: 要访问Time类内的成员,使用友元类 void SetDateTime(int hour,int minute,int second) { _t._hour = hour; _t._minute = minute; _t._second = second; } private: int _year; int _month; int _day; Time _t; }; ostream& operator<<(ostream& out, const Date& d) { out << d._year << "-" << d._month << "-" << d._day << endl; return out; }这段代码中我们可以发现,想要在Date类中访问Time类,就需要使用友元类。
注意:
- 友元类的关系是单向的(例如:在Time类中声明了Date类是友元类,那么在Date类中可以访问Time类中的非公有成员变量,反之则不能)
- 友元类的关系不具有传递性(例如:A是B的友元类,B是C的友元类,但是不能说明A是C的友元类)
5. 内部类
内部类的概念其实很简单,当一个类定义在另一个类的内部,那么这个内部的类就叫做内部类:
class A { private: static int k; int h; public: //内部类 class B//B天生是A的有友元 { public: void foo(const A& a) { cout << k << endl; cout << a.h << endl; } private: int _b; }; };然后我们来探讨一下这个内部类能干嘛?
我们先来看看这个内部类占用外部类的内存情况:
A a; cout << sizeof(a) << endl;
结果令人有些吃惊,居然只有四个字节!!!
而这个内存正是外部类int h的,换句话说,内部类似乎与外部类并没有任何关系。那么它存在的意义是什么呢?
C++中这样规定,所有内部类天生是外部类的友元(也就是说,内部类可以访问外部类的成员,但是外部类并不是内部类的友元):
class A { private: static int k; int h; public: //内部类 class B//B天生是A的有友元 { public: void foo(const A& a) { cout << k << endl; cout << a.h << endl; } private: int _b; }; void Print(const B& b) { cout << b._b; } }; int main() { A a; }
总结:
- 内部类可以定义在外部类的 public 、 protected 、 private 都是可以的
- 注意内部类可以直接访问外部类中的 static 、枚举成员,不需要外部类的对象 / 类名
- sizeof( 外部类 )= 外部类,和内部类没有任何关系
(本篇完)











1564

被折叠的 条评论
为什么被折叠?



