目录
C语言中结构体中只能定义变量,C++中结构体内不仅可以定义变量,也可以定义函数,C++中更喜欢用class来代替。区别是struct的成员默认访问方式是public,class是struct的成员默认访问方式是private。
定义方式
- 声明、定义全放类中。成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
- 声明放在.h文件中,定义放在.cpp文件中。(推荐使用)
类的访问限定符及封装
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用,本质是一种管理。
- public修饰的成员在类外可以直接被访问,protected和private修饰的成员在类外不能直接被访问
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
类对象模型
- 对象中只保存成员变量,成员函数存放在公共的代码段。
- 一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐
- 注意空类的大小,编译器给了空类一个字节来唯一标识这个类
构造函数
构造函数是一个特殊的成员函数,名字与类名相同,无返回值,可以重载,创建对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次,其主要任务是初始化对象。
使用构造函数
C++提供了显式调用和隐式调用两种使用构造函数来初始化对象的方式。
注意:如果通过隐式调用默认构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明。
class S {
public:
S(int a = 1, string s = "sss") {
_a = a;
_s = s;
}
private:
int _a;
string _s;
};
int main() {
S s1 = S(3, "Ran"); // 显示调用
S s2(3, "ran"); // 隐式调用
S s3; // S s3();为错误用法
return 0;
}
默认构造函数
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且只能有一个。
若未显式定义构造函数,则编译器会自动生成一个无参的默认构造函数,主要作用是调用自定类型成员的默认构造,一旦用户显式定义编译器将不再生成。
初始化列表
构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,在初始化列表中。而构造函数体内可以多次赋值。
- 声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
class A {
public:
A(int a)
:_a(a) {
}
private:
int _a;
};
class B {
public:
// 冒号开始,以逗号分隔的数据成员列表,
// 每个成员变量后面跟一个放在括号中的初始值或表达式。
B(int a, int ref)
:_aobj(a)
, _ref(ref)
, _n(10) {
}
private:
// 引用成员变量、const成员变量、类类型成员(该类没有默认构造函数)
// 必须放在初始化列表位置进行初始化。
A _aobj; // 没有默认构造函数
int& _ref; // 引用
const int _n; // const
};
单参构造
class S {
public:
S(int s = 1) {
_s = s;
}
private:
int _s;
};
int main() {
S obj;
obj = 1999; // 实际编译器会构造一个无名对象,最后用无名对象给obj进行赋值
return 0;
}
上述代码可读性不是很好,用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
explicit S(int s = 1) {
_s = s;
}
C++11 成员初始化
C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。即,如果初始化列表和声明的时候都有初始化参数,会优先使用初始化列表初始化。
C++11 委派构造函数
通过委派其他构造函数,使多构造函数的类编写更容易。
// 委派构造函数将构造的任务委派给目标构造函数
class test {
public:
// 目标构造函数
test()
: _type(0)
, _a('a') {
InitRSet();
}
// 委派构造函数
Info(int a)
: Info() {
_a = a;
}
// 委派构造函数
Info(char c)
: Info() {
_c = c;
}
private:
void InitRSet() {//初始化其他变量}
int _a;
char _c;
// ...
};
构造函数不能同时“委派”和使用初始化列表
拷贝构造函数
本质是构造函数的一个重载形式,参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
若未显示定义,编译器自动生成默认拷贝构造函数,默认的拷贝构造函数为浅拷贝。
若显示定义拷贝构造,则编译器不会生成默认构造函数。(拷贝构造是构造函数的重载)
析构函数
- 无参数无返回值,一个对象只有一个析构函数。
- 若未显式定义,系统会自动生成默认的析构函数,对象在销毁时会自动调用析构函数,并对自定类型成员调用它的析构函数,完成类的一些资源清理工作。
- 如果是在栈上创建多个对象,则最后创建的对象最先被删除,最先创建的对象最后被删除。
注意:申请空间时候必须自己写析构函数。
一个测试:
class S {
public:
S(int a = 1, string s = "sss") {
_a = a;
_s = s;
}
~S() {
cout << "~S()" << endl;
}
private:
int _a;
string _s;
};
int main() {
{ // 如果没有大括号,代码块将为整个main(),
// 仅当main()执行完毕调析构,在窗口环境中可能无法看到~S()
S s = S();
}
return 0;
}
输出结果 ~S()
某些编译器可能输出两个 ~S()
C++标准允许编译器使用两种方式来执行S s = S();
- 第一种等价于
S s;
创建一个对象,执行一次析构 - 第二种会创造一个匿名临时对象,然后将匿名对象复制到s中,创建两个对象,执行两次析构。
所以尽量使用S s;
这种隐式构造,通常效率更高。
运算符重载
运算符重载是一种形式的C++多态。
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型或者枚举类型的用户定义参数,防止用户为内置类型重载运算符。
- 必须遵守语法规则,如,不可将%重载成一个操作数。
- .* :: sizeof ?: . typeid const_cast dynamic_cast reinterpret_cast statci _cast 不可重载
如果需要自己写赋值运算符重载,需要检查是否自己给自己赋值。
一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的浅拷贝。
取地址及const取地址操作符重载一般不用重新定义,编译器默认会生成。
// 运算符重载演示:
class S {
public:
S(int a = 1, string s = "sss") {
_a = a;
_s = s;
}
S& operator+(S& s) {
_a += s._a;
return *this;
}
private:
int _a;
string _s;
};
int main() {
S a, b, c;
a = a + b + c;
//a = a.operator+(b).operator+(c); 和上面代码等价
return 0;
}
this指针
C++编译器给每个“成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象,函数体中所有成员变量的操作,都是通过该指针去访问。用户不需要来传递,编译器自动完成。
- this指针类型:类类型* const
- this指针是成员函数第一个隐含的指针形参,是对象调用成员函数时,将对象地址作为实参传递给this
形参,一般情况由编译器通过ecx寄存器自动传递,不可显示写出,对象中不存储this指针。 - this指针可以为空,但是不可访问成员变量。
class S {
public:
void Print() {
cout << "Print()" << endl;
}
int a;
};
int main() {
S* ptr = nullptr;
ptr->Print(); // 正确,只访问了公共代码段
cout << ptr->a << endl; // 错误,不可访问成员变量
return 0;
}
对象数组
S stuff1[4]; // 调用默认构造
S stuff2[4]{
S(0,"aaa"),
S(),
S(2,"ccc"),
}; // stuff2[1] stuff2[3] 调用默认构造
需要注意的是,初始化对象数组的过程是,先使用默认构造创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中,所以要创建类对象数组,则这个类必须要有默认构造函数。
C++11 列表初始化
C++11扩大了列表初始化的使用范围,使其可用于所有的内置类型和用户自定
义的类型,需要提供与构造函数的参数列表匹配的内容,使用时,可添加等号(=),也可不添加
内置类型的列表初始化
// 内置类型变量
int x{ 1 + 1 };
// 数组
int arr[]{ 1, 2, 3, 4, 5 };
// 动态数组,在C++98中不支持
int* arr = new int[3]{ 1, 2, 3 };
// 标准容器
vector<int> v{ 1, 2, 3, 4, 5 };
map<int, int> m{ {1, 1}, {2, 2,}, {3, 3}, {4, 4} };
自定义类型的列表初始化
S s1{};
S s2; // s1 s2等价
S s3{ 3,"ran" }; // S s3 = { 3, "ran"},可添加等号(=),也可不添加
S s4(3, "ran"); // s3 s4等价
// 直接初始化
S stuff[4]{
S(0,"aaa"),
S(),
S(2,"ccc"),
};
容器类型的列表初始化
实现vector类想要支持列表初始化,需给该类添加一个带有initializer_list类型参数的构造函数,否则Vector<int> v{ 1, 2, 3, 4, 5, 6 };
就会报错,编译器以为要创建6个对象: Vector<int> v[6]{ 1, 2, 3, 4, 5, 6 };
改进如下:
#include <initializer_list>
Vector(const initializer_list<T>& lst)
: _start(new T[lst.size()])
, _endOfStorage(_start + lst.size())
, _finish(_start) {
for (auto& e : lst) {
*_finish = e;
++_finish;
}
}
Vector<T>& operator=(initializer_list<T> lst) {
delete[] _start;
_start = new T[lst.size()];
_endOfStorage = _start + lst.size();
_finish = _start;
for (auto& e : lst) {
*_finish = e;
++_finish;
}
return *this;
}
Vector<int> v{ 1, 2, 3, 4, 5, 6 };
创建一个对象,并将成员初始化。
initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()
C++11 默认函数控制
显式缺省函数:
C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本。
删除默认函数:
在函数声明加上=delete,编译器不生成对应函数的默认版本
ps:避免删除函数和explicit一起使用
const成员函数
const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
加上const修饰后,隐式的将 类类型* const this
变成 const 类类型* const this
- 非const对象/成员函数可以调用任何成员函数
- const对象/成员函数只可以调用const成员函数
static成员
static可以声明成员函数和成员变量。
- 静态成员为所有类对象所共享,不属于某个具体的实例,静态成员变量必须在类外定义,定义时不添加static关键字。
- 类静态成员即可用类名::静态成员或者对象.静态成员来访问。
- 没有隐藏的this指针,不能访问任何非静态成员。
友元
类方法和友元只是表达类接口的两种不同机制。但友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元函数
- 友元函数可以直接访问类的私有成员
- 它是定义在类外部的普通函数,不属于任何类,不能使用成员运算符来调用
- 虽然不是成员函数,但是它与成员函数的访问权限相同。
- 一个函数可以是多个类的友元函数
- 当友元函数代码很短时,可以在声明时同时定义,可成为内联函数。
代替友元函数的办法:
class S {
public:
S& operator+(int n) {
_a += n;
return *this;
}
private:
int _a;
string _s;
};
// 这样就可以避免突破封装了
S& operator+(int n, S& s) {
return s + n;
}
int main() {
S obj;
obj = obj + 1; // obj = obj.operator+(1);
obj = 1 + obj; // 调用S& operator+(int n, S& s)
return 0;
}
友元类
- 友元类的所有成员函数都可以是另一个类的友元函数
- 单向,不可传递
class AAA; // 类的不完全声明
class BBB {
friend class AAA;
public:
BBB() :_b(3) {}
private:
int _b;
};
class AAA {
public:
void Test() {
// 可以访问私有成员
cout << _obj._b << endl;
}
private:
BBB _obj;
};
int main() {
AAA a;
a.Test(); // 3
return 0;
}
内部类
内部类是一个独立的类,它不属于外部类,可以看作外部类的友元类,外部类对内部类没有任何优越的访问权限。
- 可以定义在外部类的任何地方
- sizeof(外部类)=外部类,和内部类没有任何关系
- 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名
class AAA {
public:
class aaa {
public:
void Test() {
cout << k << endl; // 3
cout << RED << endl; // 0
cout << GREEN << endl; // 1
}
};
static int k;
enum Color {
RED,
GREEN
};
};
int AAA::k = 3;
int main() {
AAA::aaa a;
a.Test();
return 0;
}