把知识记在小本本上

将零散的知识点放在一个集中的地方,不断递归重构,形成一套为己所用的知识系统。

博客首页 | 小本本首页

类的引入

万物皆对象。类是一种用户自定义的数据类型,包括表示属性的成员变量和表示行为的成员函数,类是现实世界对象的抽象,对象是类虚拟世界的实例。

  • C++ 中 struct 中不仅可以有变量,还可以有函数。

  • 类定义结束时后面分号。

  • 成员函数声明和定义可以全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。

  • 一般定义在 .h 文件中,定义放在 .cpp 文件中。

  • 面向对象三大特性:封装、继承、多态。

访问限定符及封装

访问限定符

C++ 实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

public 公有的、protected 保护的、private 私有的。

  • public 修饰的成员在类外可以直接被访问。

  • protected 和 private 修饰的成员在类外不能直接被访问。

  • 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。

  • class 的默认访问权限为 private ,struct 默认为 public (因为struct要兼容C)。

  • 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。

封装

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

封装本质上是一种管理:我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们
首先建了一座房子把兵马俑给封装起来。但是我们目的全封装起来,不让别人看。所以我们开放了售票通
道,可以买票突破封装在合理的监管机制下进去参观。类也是一样,我们使用类数据和方法都封装到一下。
不想给别人看到的,我们使用 protected/private 把成员封装起来。开放一些共有的成员函数对成员合理的访
问。所以封装本质是一种管理。

类的实例化

用类类型创建对象的过程,称为类的实例化。

类好比是一个房子的设计图纸,类的实例化就是按照图纸建造一个房子。一个设计图可以建好几套房子。

造对象~~~。

类的对象模型

计算类对象的大小

❓ 类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?

✔️ 只保存成员变量,成员函数存放在公共的代码段。一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。

🌰 栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class A1
{
public:
void f1() {}

private:
int _a;
};
// 类中仅有成员函数
class A2
{
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{
};

int main()
{
cout << "sizeof(A1) : " << sizeof(A1) << endl; // 4
cout << "sizeof(A2) : " << sizeof(A2) << endl; // 1
cout << "sizeof(A3) : " << sizeof(A3) << endl; // 1
return 0;
}

内存对齐规则

结构体内存对齐规则:

  • 第一个成员在与结构体偏移量为 0 的地址处。

  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

    • 对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。

    • VS 中默认的对齐数为 8,gcc 中的对齐数为 4 。

  • 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

结构体的内存对齐规则适用于类。

⚠️ 类中嵌套的类是不占用空间的,只有当有了类的对象后,才会占用空间。

1
2
3
4
5
6
7
8
9
10
11
12
class A1
{
public:
void f1() {}

private:
int _a;

class A2
{
};
}; // sizeof(A1) = 4

❓ 为什么要进行内存对齐?

✔️ https://www.cnblogs.com/jijiji/p/4854581.html

❓ 如何让结构体按照指定的对齐参数进行对齐?

✔️ #pragma pack(4)

❓ 如何知道结构体中某个成员相对于结构体起始位置的偏移量?

✔️ 造个对象,地址相减。

✔️ &(((type*)0)->m)

❓ 什么是大小端?如何测试某台机器是大端还是小端?有没有遇到过要考虑大小端的场景?

✔️ 在网络通信中会涉及到。

this 指针

成员函数最终会被编译成与对象无关的普通函数。除了成员变量,丢失所有信息。

C++ 编译器给每个“成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

  • this 指针是 const 的,类型为对象的类型。

  • 只能在“成员函数”的内部使用。

  • this 指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给 this 形参。所以对象中不存储 this 指针。

  • this 指针是成员函数第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递。

6 个默认成员函数

任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。

class Date {};

  • 初始化和清理
  • 拷贝复制
    • 拷贝构造:使用同类对象初始化创建对象
    • 赋值重载:把一个对象赋值给另一个对象
  • 取地址重载
    • 主要是普通对象和 const 对象取地址,这两个很少会自己实现

构造函数

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次

  • 构造函数可以重载。

  • 为成员变量赋初始值,分配资源(是给对象的成员变量分配资源),设置对象的初始状态。

  • 函数名与类名相同,没有返回类型。

  • 对象创建时自动调用且只调用一次。

    • 栈区创建对象:对象定义语句。
    • 堆区创建对象:new 操作符。
  • 自定义成员类型要调用自己的构造函数,编译器自动生成的构造函数会自己调用它。

  • 对象创建过程:

    1. 为整个对象分配内存;
    2. 构造基类部分(如果存在基类);
    3. 构造成员变量;
    4. 执行构造函数代码。
  • 一般访问属性为 public,除非我们不允许外部创建对象。

⚠️

  • 构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是给对象开空间创建对象,而是初始化对象,给对象里的成员变量开空间、赋值
  • 如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明。如Date d3();,声明了d3函数,该函数无参,返回一个日期类型的对象。
  • 如果类中没有显式定义构造函数,则编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
  • 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
  • 无参构造和全缺省构造都称为默认构造函数,并且默认只能有一个。

成员变量命名风格

m_name_age,具体看要求~,主要是和参数做区分。

析构函数

与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

  • 无参数无返回值。
  • 只有一个析构函数,不能重载。
  • 负责对象销毁时回收对象占用资源。
  • 自定义成员类型要调用自己的析构,编译器自动生成的析构函数会自己调用它。
  • 在析构函数中 delete / free 构造函数中 new / malloc 的东西。
  • 在用完对象后 delete 对象,调用析构函数,当对象的生命周期后,自动调用析构函数。

⚠️ 析构函数中,释放空间前要判断要释放的空间是否为 NULL / nullptr,释放完后要指向空,避免野指针。

拷贝构造

用已存在的类类型对象创建新对象时由编译器自动调用。

  • 拷贝构造函数是构造函数的一个重载形式。

  • 参数只有一个且必须使用引用传参(并且加 const 修饰),使用传值方式会引发无穷递归调用。

  • 如果一个类没有定义拷贝构造函数,那么编译器提供一个默认拷贝构造函数(public)。

  • 系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷
    贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

    例如:成员变量中有一个char *的成员变量,系统生成的默认拷贝构造会拷贝这个指针变量的值(指向的地址)给新的对象,这样,这两个对象就指向的同一块空间,在析构的时候会出现问题。

类型转换构造

构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Date {
public:
Date(int year):_year(year){}
private:
int _year;
int _month:
int _day;
};
void TestDate()
{
Date d1(2018);
// 用一个整形变量给日期类型对象赋值
// 实际编译器背后会用2019构造一个无名对象,最后用无名对象给d1对象进行赋值
d1 = 2019;
}

上述代码可读性不是很好,用 explicit 修饰构造函数,将会禁止单参构造函数的隐式转换。

1
explicit Date(int year):_year(year){}

运算符重载

  • 不能通过连接其他符号来创建新的操作符:比如 operator@。
  • 重载操作符必须有一个类类型或者枚举类型的操作数,int operator+(int a, int b) {},就是不可以的。
  • 作为类成员的重载函数时,其形参看起来比操作数数目少 1 成员函数的操作符有一个默认的形参 this,限定为第一个形参,bool operator==(Date* this, const Date& d2),调用cout<<(d1 == d2)<<endl;相当于d1.operator==(d2)
  • 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
  • #->*.*::sizeof?:. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

⚠️ ..*运算符不能重载是为了保证访问成员的功能不能被改变,域运算符合sizeof运算符的运算对象是类型而不是变量或一般表达式,不具备重载的特征。

❓ 运算符重载成全局的就需要成员变量是共有的,封装性如何保证?

✔️ 友元或者直接重载成成员函数。

⚠️

⚠️ 重载前置++(Complex& operator++())和后置++(Complex operator++(int),哑元占位,用来区分)这种的运算符应注意!

const

const 修饰类的成员变量

必须使用初始化参数列表初始化,初始化之后不能修改。

1
2
3
4
5
6
7
class A
{
A():m_a(10){} //常成员变量必须用这种方式赋初始值
~A(){}
private:
const int m_a;
};

const 修饰类的成员函数

将 const 修饰的类成员函数称之为 const 成员函数,const 修饰类成员函数,实际修饰该成员函数隐含的 this 指针,表明在该成员函数中不能对类的任何成员进行修改,也叫常函数。

⚠️ 常函数内部无法修改成员变量的值,除非 mutable 修饰该成员变量。

void Display() const{...} <=> void Display(const Date* this){...}

const 对象

  • 这个对象里的成员变量是无法修改的。

  • 被const修饰的对象,对象指针或对象引用,统称为常对象

  • 常对象只能调用常函数,非常对象即可调用常函数,也可调用非常函数,优先调用非常版本。

  • 成员函数常版本和非常版本可以构成重载。

⚠️ 区别的标记并不是谁离得近,而是他在*号的前面还是后面。

1
2
3
const Person* p = &p1;//对象是const
Person const* p = &p1;//对象是const
Person *const p = &p1;//指针是const

取地址及 const 取地址操作符重载

这两个默认成员函数一般不用重新定义,编译器默认会生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};

⚠️ 这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比
想让别人获取到指定的内容!

再谈构造函数

1
2
3
4
5
6
7
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造
函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内
可以多次赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person { ... };
class Currency { ... };

class SavingsAccount
{
public:
SavingsAccount(const char* name,const char* address,int cents); // 这里是不合适的
// Person and Currency都有自己的构造函数,应该由自己来初始化自己,而不是SavingsAccount来构造
~SavingAccount();
void print();
private:
Person m_saver;
Currency m_balance;
};

初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个”成员变量”后面跟一个放在括 号中的初始值或表达式。

1
2
3
4
5
6
7
8
// So:
SavingsAccount::SavingsAccount(const char * name,const char* address,int cents):m_saver(name,address),m_balance(0,cents){}

void SavingsAccount::print()
{
m_saver.print(); //print() in saver
m_balance.print(); //print() in balance
}

⚠️

  • 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。
  • 类中包含以下成员,必须放在初始化列表位置进行初始化:
    • 引用成员变量(引用在定义的是时候必须初始化)
    • const 成员变量(和上面一个道理,const在定义的时候必须初始化一个值)
    • 类类型成员(该类没有默认构造函数,如果有,要通过自己的构造函数来初始化)
  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

C++ 11成员初始化新方式

非静态成员变量,可以在成员声明时,直接初始化。

相当于给无参构造函数一个缺省参数。

static 成员

声明为 static 的类成员称为类的静态成员,用 static 修饰的成员变量,称之为静态成员变量;用 static 修饰的成员函数,称之为静态成员函数。

面试题:实现一个类,计算中程序中创建出了多少个类对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A
{
public:
A() {++_scount;}
A(const A& t) {++_scount;}
static int GetACount() { return _scount;}
private:
static int _scount;
};
int Test::_count = 0;
void TestA()
{
cout<<A::GetACount()<<endl;
A a1, a2;
A a3(a1);
cout<<A::GetACount()<<endl;
}

⚠️

  • 静态的成员变量一定要在类外进行初始化。
  • 静态成员为所有类对象所共享,不属于某个具体的实例。
  • 静态成员变量必须在类外定义,定义时不添加static关键字。
  • 类静态成员即可用类名::静态成员或者对象.静态成员来访问。
  • 静态成员函数没有隐藏的 this 指针,不能访问任何非静态成员。
  • 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值,const修饰符等参数。

❓ 静态成员函数可以调用非静态成员函数吗?

✔️ 不可以,非静态的成员函数默认有一个参数(this指针),但是静态成员函数没有这个 this 指针传给他,所以不可以调用。

❓ 非静态成员函数可以调用类的静态成员函数吗?

✔️ 可以。

友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元函数

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加 friend 关键字。

  • 友元函数可访问类的私有成员,但不是类的成员函数
  • 友元函数不能用 const 修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用和原理相同

问题:现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为 cout 的输出流对象和隐含的this指针在抢占第一个参数的位置。this 指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day) {}
ostream& operator<<(ostream& _cout)
{
_cout<<d._year<<"-"<<d._month<<"-"<<d._day;
return _cout;
}
private:
int _year;
int _month;
int _day
};
int main() {
Date d(2017, 12, 24);
d<<cout; // 这种使用方式很别扭,不符合使用习惯
return 0;
}

使用友元修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, const Date& d);
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout<<d._year<<"-"<<d._month<<"-"<<d._day;
return _cout;
}
istream& operator>>(istream& _cin, const Date& d)
{
_cin>>d._year;
_cin>>d._month;
_cin>>d._day;
return _cin;
}
int main() {
Date d;
cin>>d;
cout<<d<<endl;
return 0;
}

友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

  • 友元关系是单向的,不具有交换性。

    我把你当朋友,带你去我家吃好吃的,你却不把我当朋友,不带我去你家。

  • 友元关系不能传递。

    如果 B 是 A 的友元,C 是 B 的友元,则不能说明 C 时 A 的友元。

内部类

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。

  • 内部类可以定义在外部类的 public、protected、private 都是可以的。
  • 内部类可以直接访问外部类中的 static、枚举成员,不需要外部类的对象/类名。
  • sizeof(外部类)=外部类,和内部类没有任何关系。

⚠️ 内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A
{
private:
static int k;
int h;
public:
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}