- 一、auto
- 二、引用
- 1.引用的使用
- 2.左值引用和右值引用
- 三、new与delete
- 1.new的使用
- 2.delete与delete []
- 四、函数重载
- 五、string
- 六、vector
- 1.vector基本使用
- 2.vector的内存分配策略
- 七、类型转换
- 1.static_cast
- 2.dynamic_cast
- 3.reinterpret_cast
- 4.const_cast
- 八、构造函数和析构函数
- 1.构造函数与析构函数的使用
- 2.深拷贝和浅拷贝
- 3.初始化列表
- 4.空类默认生成的成员
- 九、static
- 十、const与constexpr
- 十一、友元
- 十二、运算符重载
- 十三、继承
- 1.三种继承方式
- 2.多重继承的二义性
- 3.向上转型
- 4.多态
- 5.纯虚函数与抽象类
- 十四、map
通过auto声明的变量会在声明时自动进行类型推导(类似于js等弱类型语言中的let),这个类型推导会发生在编译过程中,因此不会影响运行效率。在很多情况下auto可以简化代码,但也会降低程序的可读性。
auto主要用于代替冗长复杂的变量声明:
#include#include
二、引用 1.引用的使用
引用是一个变量的别名,也就是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名或变量名来操作该变量,操作引用就相当于在操作引用所绑定的变量。
引用本质上相当于一个常指针:
int &a = b; int *const a = &b;
引用与指针主要有以下区别:
①定义引用必须绑定变量,即引用必须连接到一块合法的内存,不存在空引用。
②一旦引用被初始化为一个对象,就不能被指向到另一个对象;而指针可以在任何时候指向到另一个对象。
③引用必须在创建时被初始化,而指针可以在任何时候被初始化。
#includevoid swap(int &a, int &b) { a = a + b; b = a - b; a = a - b; } int main (int argc, char *argv[]) { int x = 0, y = 1; swap(x, y); std::cout << "x = " << x << ", y = " << y << std::endl; return 0; }
Atreus@AtreusdeMBP CppCode % make clang++ -c main.cpp clang++ *.o -o main Atreus@AtreusdeMBP CppCode % ./main x = 1, y = 0
此外,如果想通过引用提高程序的运行效率,同时保护传入值避免意外修改,可以以const 类型 & 引用名的形式传入一个常引用。
引用也可以作为函数的返回值,引用作为函数的返回值时,不能返回局部变量、局部对象、局部数组的引用,但可以返回一个全局变量或者静态变量的引用,主要有以下四种使用方式:
①不接收返回值。
②用一个普通变量接收函数的返回值,这时接收的是变量的值而不是变量的引用。
③用一个引用来接收函数的返回值,接收的是一个引用。
④当成左值来使用。
#include2.左值引用和右值引用using namespace std; int num = 1; int &fun() { return num; } int main(int argc, char const *argv[]) { fun(); int a = fun(); cout << a << endl; //1 a = 10; cout << num << endl; //1 int &b = fun(); cout << b << endl; //1 b = 10; cout << num << endl; //10 fun() = 100; cout << a << endl; //10 cout << b << endl; //100 cout << num << endl; //100 return 0; }
在C++11中增加了左值引用和右值引用的概念。
左值是可以放在赋值号左边,可以被赋值的值,左值必须要在内存中有实体。右值是在赋值号右边,取出值赋给其他变量的值,右值可以在内存也可以在寄存器。一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
左值引用就是一级引用类型 &引用名 = 左值表达式,它必须被绑定到一个左值上,而不能被绑定到表达式或者字面常量等右值上。右值引用是二级引用类型 &&引用名 = 右值表达式,它必须被绑定到一个右值上。
#includeusing namespace std; int main(int argc, char const *argv[]) { int num = 1; int &l_ref = num; int &&r_ref = num * 2; cout << "l_ref = " << l_ref << endl; //1 cout << "r_ref = " << r_ref << endl; //2 return 0; }
三、new与delete 1.new的使用
new与malloc一样,都是用来动态分配内存,二者主要区别如下:
| new | malloc | |
|---|---|---|
| 分配区域 | 在自由存储区上进行内存空间的分配,自由存储区默认是堆,但也可以是静态存储区等其他内存区域 | 在堆上动态分配内存 |
| 分配大小 | 申请内存分配时无须指定内存块的大小,会按照指定数据类型自动进行分配 | 分配内存按照指定字节数进行分配 |
| 分配成功 | 内存分配成功时,返回的是指定类型的指针,无需进行类型转换 | 内存分配成功,返回的是void *类型的指针,需要强制类型转换为我们所需要的类型 |
| 分配失败 | 内存分配失败时会抛出一个bac_alloc异常而不会返回NULL,需要通过捕获异常来判断内存分配是否成功 | 内存分配失败时会直接返回NULL |
| 构造与析构操作 | 内存分配成功后需要通过delete来销毁内存,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构 | 内存分配成功后需要通过free来销毁内存 |
| 初始化操作 | 分配内存空间的同时可以实现对象的初始化 | 无法实现初始化 |
就像malloc与free一样,通过new分配的内存必须手动释放以避免内存泄漏。
int *p0 = new int(5);
// ······
delete p0;
p0 = nullptr;
int *p1 = new int[5]{1, 2, 3, 4, 5};
// ······
delete []p1;
p1 = nullptr;
int **p2 = new int*[5];
for (int i =0; i < 5; i++) {
p2[i] = new int[5];
}
// ······
for (int i = 0; i < 5; i++) {
delete []p2[i];
p2[i] = nullptr;
}
delete []p2;
p2 = nullptr;
2.delete与delete []
delete与delete []均可用于释放new所分配的内存,其中delete释放的一般是new分配的单个对象指针指向的内存,而delete []释放一般是new 类型[]分配的数组的指针指向的内存。实际上通过以下代码的执行结果可以发现,对于简单的基本数据类型,二者都能够成功地将内存释放(不过并不推荐用delete释放数组),但是对于一些自定义数据类型的数组比如对象数组,使用delete时只会调用数组中首个对象的析构函数,其余元素的析构函数都无法执行,这对于一些通过析构函数释放文件描述符等系统资源的对象来说可能会导致系统资源的耗尽。
#includeusing namespace std; int main(int argc, char *argv[]) { int *p1 = new int[3]{1, 2, 3}; for (int i = 0; i < 3; i++) cout << "p1[" << i << "] = " << p1[i] << endl; delete []p1; for (int i = 0; i < 3; i++) cout << "p1[" << i << "] = " << p1[i] << endl; int *p2 = new int[3]{1, 2, 3}; for (int i = 0; i < 3; i++) cout << "p2[" << i << "] = " << p2[i] << endl; delete p2; for (int i = 0; i < 3; i++) cout << "p2[" << i << "] = " << p2[i] << endl; return 0; }
Atreus@AtreusdeMBP code % clang++ -std=c++11 main.cpp -o main
main.cpp:12:5: warning: 'delete' applied to a pointer that was allocated with 'new[]'; did you mean 'delete[]'? [-Wmismatched-new-delete]
delete p2;
^
[]
main.cpp:10:15: note: allocated with 'new[]' here
int *p2 = new int[3]{1, 2, 3};
^
1 warning generated.
Atreus@AtreusdeMBP code % ./main
p1[0] = 1
p1[1] = 2
p1[2] = 3
p1[0] = -1553416144
p1[1] = 39260
p1[2] = 2043
p2[0] = 1
p2[1] = 2
p2[2] = 3
p2[0] = -1553416144
p2[1] = 39260
p2[2] = 2043
四、函数重载
在C语言中,经常需要编写一些功能相似但是细节不同的函数,这就降低了函数名的可读性,由此引出了函数重载的概念。
函数重载的注意事项:
①函数名相同。
②函数的参数的个数、类型、顺序不同。
③返回值类型不能作为重载的标准。
④默认参数也会影响重载条件,但常量不能作为判定重载的标准
#includeusing namespace std; int add(int a, int b) { cout << "int add(int a, int b) return "; return a + b; } int add(int a, int b, int c) { cout << "int add(int a, int b, int c) return "; return a + b + c; } char add(char a, char b) { cout << "char add(char a, char b) return "; return a + b; } int main(int argc, char *argv[]) { cout << add(1, 2) << endl; cout << add(1, 2, 3) << endl; cout << add('0', '2') << endl; //分别对应ASCII码48 50 98 return 0; }
Atreus@AtreusdeMBP code % clang++ main.cpp -o main Atreus@AtreusdeMBP code % ./main int add(int a, int b) return 3 int add(int a, int b, int c) return 6 char add(char a, char b) return b
五、string
string实际上封装了C语言中使用 终止的一维字符数组,并提供了一系列对其进行操作的方法。
#include#include using namespace std; int main(int argc, char *argv[]) { string s("Hello world!"); //等同于string s = "Hello world!"; printf("s.c_str() : %sn", s.c_str()); printf("s.data() : %sn", s.data()); cout << "s.size() : " << s.size() << endl; //size方法返回string对象中的元素数量 cout << "s.length() : " << s.length() << endl; //length方法与size方法返回值没有区别 length是因为沿用C语言的习惯而保留下来的 cout << "s.max_size() : " << s.max_size() << endl; //当前环境最多可容纳多少字符 但string是可变长的 cout << "s.capacity() : " << s.capacity() << endl; //capacity方法返回string对象的实际容量 实际容量除了字符串字符长度以外还包括一些预留空间以减少内存分配次数 cout << "s[0] : " << s[0] << endl; //[]是运算符 越界不报错 cout << "s.at(0) : " << s.at(0) << endl; //at方法会进行越界检查 string::iterator it; it = s.begin(); //begin方法返回指向字符串第一个元素的迭代器 cout << "*s.begin() : " << *it << endl; it = s.end() + 1; //end方法返回指向字符串最后一个字符下一个位置' '的迭代器 cout << "*s.end() * 1 : " << *it * 1 << endl; return 0; }
Atreu@AtreusdeMBP code % make clang++ -c main.cpp clang++ *.o -o main Atreu@AtreusdeMBP code % ./main s.c_str() : Hello world! s.data() : Hello world! s.size() : 12 s.length() : 12 s.max_size() : 18446744073709551599 s.capacity() : 22 s[0] : H s.at(0) : H *s.begin() : H *s.end() * 1 : 0
#include#include using namespace std; int main(int argc, char *argv[]) { string s, s_; s = "You have a dream, you got to protect it."; s_ = "X"; cout << "s.compare(s_) : " << s.compare(s_) << endl; //1 s_ = "Z"; cout << "s.compare(s_) : " << s.compare(s_) << endl; //-1 s_ = s; cout << "s.compare(s_) : " << s.compare(s_) << endl; //0 s = "Hello"; s_ = " world!"; cout << "s + s_ : " << s + s_ << endl; //Hello world! s.append(1, 32); //向s末尾追加一个空格字符 cout << "s.append(3, 32) : " << s << endl; //Hello s.append(s_, 1, 6); //向s末尾追加s_从下标1开始的6个字符 cout << "s.append(s_, 1, 5) : " << s << endl; //Hello world! s = "Uneasy lies the head that wears a crown."; if (s.find("n") == string::npos) { cout << "s.find("n") : nposn"; } else { cout << "s.find("n") : " << s.find("n") << endl; //1 } if (s.find("easy") == string::npos) { cout << "s.find("easy") : nposn"; } else { cout << "s.find("easy") : " << s.find("easy") << endl; //2 } if (s.find("x") == string::npos) { cout << "s.find("x") : nposn"; //npos } else { cout << "s.find("x") : " << s.find('x') << endl; } if (s.rfind("n") == string::npos) { cout << "s.rfind("n") : nposn"; } else { cout << "s.rfind("n") : " << s.rfind("n") << endl; //38 } s = "I miss you."; s_ = "I hate you."; s.replace(2, 4, "hate"); //以hate取代s从下标2开始4个字符 cout << "s.replace(2, 4, "hate") : " << s << endl; //I hate you. s.replace(2, 4, s_, 2, 4); //以s_从下标2开始4个字符取代s从下标2开始4个字符 cout << "s.replace(2, 4, s_, 2, 4) : " << s << endl; //I hate you. s = "Don't ever let somebody tell you you can't do something, not even me."; s.erase(6, 5); //从下标6开始删除5个字符 cout << "s.erase(6, 5)" << s << endl; //Don't let somebody tell you you can't do something, not even me. s.erase(5); //删除从下标5开始后面的所有字符 cout << "s.erase(5)" << s << endl; //Don't return 0; }
六、vector
vector(向量)是一个封装了动态大小数组的顺序容器。和其它容器一样,vector能够存放各种类型的对象,可以简单地将它看作是一个能够存放任意类型元素的动态数组,它也可以随机存取数据(通过[]操作符或者at()方法)。vector在尾部添加或移除元素非常快速,但是那个中间插入元素时可能会导致很多后续的元素要重新构造,效率比较低。
1.vector基本使用#include#include using namespace std; int main(int argc, char *argv[]) { vector v = {"Sakura", "Pilot", "Kokuyo"}; for (int i = 0; i < v.size(); ++i) { cout << v[i] << " "; } cout << "n"; for (auto it = v.begin(); it != v.end(); ++it) { cout << *it << " "; } cout << "n"; for (auto it = v.rbegin(); it != v.rend(); ++it) { cout << *it << " "; } cout << "n"; return 0; }
Atreus@AtreusdeMBP code % clang++ -std=c++11 main.cpp -o main Atreus@AtreusdeMBP code % ./main Sakura Pilot Kokuyo Sakura Pilot Kokuyo Kokuyo Pilot Sakura2.vector的内存分配策略
向vector中插入元素时,如果已分配的空间不足,不会在后面直接追加分配,而是重新分配所有的空间然后依次拷贝原vector中的数据。因此可以通过reserve()为vector预留空间以减少拷贝次数,同时也可以通过emplace_back()原地构造,以避免每次插入时创建临时对象。
#include#include using namespace std; class Car { public: int m_price; explicit Car(int price) : m_price(price) { cout << "Car(" << price << ")" << endl; } Car(const Car& other) { cout << "copy Car(" << other.m_price << ")" << endl; this->m_price = other.m_price; } ~Car() { cout << "~Car(" << m_price << ")" << endl; } }; int main(int argc, char *argv[]) { vector v; // v.reserve(10); v.push_back(Car(1)); //v.emplace_back(1); cout << "---" << endl; v.push_back(Car(2)); //v.emplace_back(2); cout << "---" << endl; }
Atreus@AtreusdeMBP code % make clang++ -c main.cpp -std=c++11 clang++ *.o -o main Atreus@AtreusdeMBP code % ./main Car(1) 构造临时对象 copy Car(1) 从临时对象向vector拷贝 ~Car(1) 析构临时对象 --- Car(2) 构造临时对象 copy Car(2) 从临时对象向新vector拷贝 copy Car(1) 从原vector向新vector拷贝 ~Car(1) 析构原vector中的对象 ~Car(2) 析构临时对象 --- ~Car(2) 析构新vector中的对象 ~Car(1) 析构新vector中的对象
七、类型转换
C语言中的类型转换只需要在变量前面加上括号和变量类型,但是这种转换方式对于C++并不合适:首先C中的强制类型转换无法对C++中的类进行操作,其次C语言可以在任意类型之间转换,比如可以将一个指向const型对象的指针转换为指向一个非const型对象的指针,这对类型检查非常严格的C++来说显然是不合理的,所以C++提供了下面这4种特有的类型转换操作符,四者的使用方法均为类型转换操作符<目标类型>(源表达式)。
1.static_caststatic_cast类似于 C 语言中的强制类型转换,可以进行无条件类型转换。但由于static_cast是在编译期间进行转换的,因此无法通过运行时类型检查来保证安全性。
主要应用场景如下:
①用于基本类型间的转换,但不能用于普通指针之间的转换。
②用于有继承关系的对象之间的转换和类指针之间的转换,这时向上转型是安全的,但向下转型是不安全的。
③用于将空指针转换成目标类型的空指针。
④用于将任何类型的表达式转换为空类型,但不能去除类型的const或者volatile属性。
用于基类和派生类之间的向上向下类型转换,但是在运行期间进行转换的,因此会进行类型检查,在转换失败时会返回null。但dynamic_cast不能用于基本数据类型之间的转换。
3.reinterpret_castreinterpret_cast只是重新解释类型,没有二进制的转换。
主要应用场景如下:
①改变指针或引用的类型。
②将指针或引用转换为一个足够长度的整型。
③将整型转换为指针或引用类型。
用于取出 const 属性,去掉类型的const或者volatile属性,将const类型的指针变为非const类型的指针。
八、构造函数和析构函数 1.构造函数与析构函数的使用构造函数是一种特殊的类成员函数,在创建时被调用。构造函数的名称和类名相同,但通过函数重载,可以创建多个同名的构造函数,条件是每个函数的特征标都不同。
当对象被删除时,程序将调用析构函数。析构函数没有返回类型(甚至连void都不是),也没有参数,无法重载,也因此每个类都只能有一个析构函数。
#includeclass Student { private: int m_id; public: Student(int id) { m_id = id; std::cout << "init student" << m_id << std::endl; } Student() { m_id = 0; std::cout << "init student" << m_id << std::endl; } ~Student() { std::cout << "free student" << m_id << std::endl; } }; int main(int argc, char *argv[]) { Student student1 = Student(1); Student student2(2); Student *student3 = new Student(3); delete student3; return 0; }
Atreus@AtreusdeMacBook-Pro code % make clang++ -std=c++11 -c main.cpp clang++ *.o -o main Atreus@AtreusdeMacBook-Pro code % ./main init student1 init student2 init student3 free student3 free student2 free student12.深拷贝和浅拷贝
拷贝构造函数还存在浅拷贝和深拷贝的问题。浅拷贝和深拷贝都可以实现对对象的复制,在不涉及指针等内存分配问题时二者并无区别。但如果类中存在动态内存分配,浅拷贝只会简单复制指针,这就导致析构函数执行时会连续delete相同的一段内存两次,而深拷贝会重新申请一段独立的内存空间以避免内存泄漏。
#includeclass Student { private: int m_id; char *m_name; public: Student(int id, const char *name) { this->m_id = id; this->m_name = new char[10]; std::strcpy(this->m_name, name); std::cout << "Student(int id, const char *name)" << std::endl; } Student(const Student & other) { this->m_id = other.m_id; this->m_name = other.m_name; // this->m_name = new char[10]; // std::strcpy(this->m_name, other.m_name); std::cout << "Student(const Student & other)" << std::endl; } ~Student() { delete []this->m_name; this->m_name = nullptr; std::cout << "~Student()" << std::endl; } }; int main(int argc, char *argv[]) { Student student(100, "rakan"); Student student_(student); return 0; }
Atreus@AtreusdeMBP code % make clang++ -c main.cpp -std=c++11 clang++ *.o -o main Atreus@AtreusdeMBP code % ./main Student(int id, const char *name) Student(const Student & other) ~Student() main(1574,0x101154580) malloc: *** error for object 0x6000012d8040: pointer being freed was not allocated main(1574,0x101154580) malloc: *** set a breakpoint in malloc_error_break to debug zsh: abort ./main3.初始化列表
在C++中,对象的成员变量的初始化动作发生在进入构造函数本体之前。在执行构造函数的函数体之前会首先调用该成员变量的默认构造函数(所以下例中尽管Student类的构造函数未对m_b成员进行操作,但是m_b的构造函数仍然被执行),在进入函数体之后才会调用该成员变量指定的构造函数(所以下例中m_b总共执行了两次构造函数),因此,如果我们使用初始化列表可以减少调用构造函数的过程,提高程序的效率。
就算不考虑效率,以下三种对象成员变量只能通过初始化列表初始化:
①常量成员,即const成员变量,因为常量成员只能初始化不能进行赋值。
②引用成员,因为引用也必须在定义的时候初始化,并且不能重新赋值。
③没有默认构造函数的类成员,因为使用初始化列表可以避免自动调用默认构造函数来初始化。
#includeclass A { public: int m_data; A(int data) { this->m_data = data; std::cout << "A(int data)" << std::endl; } }; class B { public: int m_data; B() { std::cout << "B()" << std::endl; } B(int data) { this->m_data = data; std::cout << "B(int data)" << std::endl; } }; class Student { private: const int m_id; //常量成员 int &m_age; //引用成员 A m_a; //没有默认构造函数的类成员 B m_b; //有默认构造函数的类成员 public: Student(int id, int &age) : m_id(id), m_age(age), m_a(22) { m_b = B(22); std::cout << "Student(int id, int &age)" << std::endl; } void show() { std::cout << "m_id : " << this->m_id << std::endl; std::cout << "m_ref : " << this->m_age << std::endl; std::cout << "m_a.m_data : " << this->m_a.m_data << std::endl; std::cout << "m_b.m_data : " << this->m_b.m_data << std::endl; } }; int main(int argc, char *argv[]) { int id = 22, age = 22; Student student(id, age); student.show(); return 0; }
Atreus@AtreusdeMBP code % make clang++ -c main.cpp -std=c++11 clang++ *.o -o main Atreus@AtreusdeMBP code % ./main A(int data) B() B(int data) Student(int id, int &age) m_id : 22 m_ref : 22 m_a.m_data : 22 m_b.m_data : 224.空类默认生成的成员
在C++中,对于一个空类,不对其作任何操作,以下八个默认函数会在有需要时由编译器默认生成,他们都是inline且public的:
#includeclass Student { public: int m_id; void show() const { std::cout << "m_id : " << this->m_id << std::endl; } // inline Student(); //1.默认构造函数 // inline Student(const Student &); //2.复制构造函数 // inline Student &operator=(const Student &); //3.赋值运算符 // inline Student *operator&(); //4.地址运算符 // inline const Student *operator&() const; //5.取址运算符const // inline Student(const Student &&); //6.移动构造函数 // inline Student &operator=(const Student &&); //7.移动赋值运算符 // inline ~Student(); //8.默认析构函数 }; Student operator+(Student &a, Student &b) { Student ret; ret.m_id = a.m_id + b.m_id; return ret; } int main(int argc, char *argv[]) { Student student1 = Student(); student1.m_id = 1; student1.show(); //1 Student student2 = Student(student1); student2.show(); //1 Student student3 = student1; student3.show(); //1 Student *student4 = &student1; student4->show(); //1 const Student *student5 = &student1; student5->show(); //1 Student student6 = Student(student1 + student2); student6.show(); //2 Student student7 = student1 + student2; student7.show(); //2 return 0; }
九、static
C/C++中static关键字主要有以下作用:
①修饰局部变量或者局部对象,可以延长该变量或者对象的生命周期。
②修饰全局变量、全局对象及普通函数,则其只能在本文件中访问而不能在其他文件中访问。
③修饰类的成员变量,该变量为静态变量(也称为类变量),属于类而不属于实例化后的对象,也不占用对象的空间,但可以被所有实例化后的对象共享访问。
④修饰成员函数,该函数为静态成员函数,属于类而不属于实例化后的对象,无法通过this指针访问,函数中不能访问类的非静态成员变量。
#includeclass Student { public: static int age; static void fun() { std::cout << Student::age << std::endl; } }; int Student::age = 0; //静态成员变量要在函数外单独初始化 int main() { Student stu; std::cout << Student::age << std::endl; //0 Student::age = 22; Student::fun(); //2 return 0; }
十、const与constexpr
C语言中const修饰的是只读变量,它的值不能通过变量名来修改,但可以通过指针修改。
在C++中:
①const修饰的普通变量是真正的常量,const常量会被编译器放入到符号表中,类似于宏,不占用内存,符号表存储的是一系列键值对。编译器不会为const常量分配空间,但当我们对一个常量进行取地址时,操作系统会分配一段内存,并且用这个常量来填充。
②const修饰成员函数,该函数只能访问而不能修改成员变量,如果需要修改时,需要用mutable修饰该变量。
③const修饰一个对象,称之为常对象,只能调用const修饰的成员函数。
#includeclass Student { public: mutable int id = 0; int age = 0; void fun() const { // this->age++; //increment of member ‘Student::age’ in read-only object std::cout << this->age << std::endl; //0 this->id++; std::cout << this->id << std::endl; //1 } void fun_() {} }; int main() { const Student stu; stu.fun(); // stu.fun_(); //passing ‘const Student’ as ‘this’ argument discards qualifiers return 0; }
const并未区分出编译期常量和运行期常量,所以在C++11以后可以通过constexpr关键字定义编译期常量来提高运行效率,而且constexpr将常量用宏来实现,但没有额外开销,更安全可靠。
const int com_val = val; const int run_val = 1; constexpr int run_val_ = 1;
十一、友元
友元是一种允许非类成员函数访问类的非公有成员的一种机制。我们即可以把一个函数(可以是另一个类的成员函数)指定为类的友元,也可以把整个类指定为另一个类的友元。友元的作用在于提高程序的运行效率(即减少了类型和安全性检查及调用的时间),但也会破坏类的封装性。
#includeclass Student { private: int m_age = 22; public: friend class Group; friend void showStudentAge(Student &student); }; class Group { public: Student m_leader; void showLeaderAge() { std::cout << this->m_leader.m_age << std::endl; } }; void showStudentAge(Student &student) { std::cout << student.m_age << std::endl; } int main() { Student student; Group group; group.m_leader = student; showStudentAge(student); //22 group.showLeaderAge(); //22 return 0; }
十二、运算符重载
运算符重载允许把标准运算符(如+、-、*、/等)应用于自定义数据类型对象。运算符重载本质上是函数重载,只是函数调用的一种方式,直观自然,可以提高程序的可读性。
运算符重载可以通过成员函数重载或者友元函数重载实现,注意事项如下:
①运算符重载只能重载已有的运算符。
②运算符重载不能改变运算符操作对象的个数、优先级以及结合性。
③运算符::、?:、.、.*、sizeof不允许重载。
④双目运算符=、()、[]、->与类型转换运算符只能以成员函数方式重载,流运算符<<与>>只能以友元函数的方式重载。
#includeclass Goods { private: double m_price; public: explicit Goods(double price) : m_price(price) {} friend Goods operator+(const Goods &g1, const Goods &g2); //通过友元函数重载 + Goods operator-(const Goods &g) const; //通过成员函数重载 - friend std::ostream &operator<<(std::ostream &out, const Goods &g); //通过友元函数重载 << }; Goods operator+(const Goods &g1, const Goods &g2) { return Goods(g1.m_price + g2.m_price); } Goods Goods::operator-(const Goods &g) const { return Goods(this->m_price - g.m_price); } std::ostream &operator<<(std::ostream &out, const Goods &g) { out << g.m_price; return out; } int main() { Goods goods1(10); Goods goods2(20); goods1 = goods1 + goods2; std::cout << goods1 << std::endl; //30 goods1 = operator+(goods1, goods2); std::cout << goods1 << std::endl; //50 goods1 = goods1 - goods2; std::cout << goods1 << std::endl; //30 return 0; }
十三、继承 1.三种继承方式
继承分为公有继承、保护继承与私有继承,除了共有继承剩下两个很少用到,三者区别如下:
2.多重继承的二义性当类B和C同时继承于基类A,类D同时继承于B和C,在D中调用A中的成员会导致二义性。通过虚基类和虚继承继承机制,实现了在多继承中只保留一份共同成员,从而解决了多继承导致的二义性问题。
#include3.向上转型class A { public: int m_a; A(int a) : m_a(a) { std::cout << "A(int a)" << std::endl; } }; class B : virtual public A { public: int m_b; B(int b) : m_b(b), A(1) { std::cout << "B(int b)" << std::endl; } }; class C : virtual public A { public: int m_c; C(int c) : m_c(c), A(1) { std::cout << "C(int c)" << std::endl; } }; class D : public B, public C { public: int m_d; D(int d) : m_d(d), B(2), C(3), A(1) { std::cout << "D(int d)" << std::endl; } }; int main() { D d(4); std::cout << d.m_d << std::endl; return 0; }
C++支持向上转型,即可以将派生类对象赋值给基类对象,也可以将派生类指针赋值给基类指针。
#include4.多态class Parent { public: Parent() { std::cout << "Parent()" << std::endl; } }; class Child : public Parent { public: Child() { std::cout << "Child()" << std::endl; } }; int main() { Parent parent; Child child; parent = child; //可以向上转型 // child = parent; //error: no viable overloaded '=' 无法向下转型 Parent *p_parent = new Parent(); Child *p_child = new Child(); p_parent = p_child; //可以向上转型 // p_child = p_parent; //error: incompatible pointer types assigning to 'Child *' from 'Parent *' 无法向下转型 return 0; }
虚函数解决了向上转型的完整性,在发生函数覆盖时可以通过基类的指针调用派生类的方法,进而实现了多态,提高了代码的扩展性。
多态的实现主要有三个前提:
①存在继承。
②存在函数覆盖(重写)。
③存在基类指针或者引用指向子类对象。
多态的实现要借助于虚函数,虚函数的限制如下:
①只有类的成员函数才能声明为虚函数。
②静态成员函数不能是虚函数。
③内联函数不能是虚函数。
④构造函数不能为虚函数。
⑤基类的析构函数可以是虚函数切通常声明为虚函数。
当类声明中包含纯虚函数时,则不能创建该类的对象,这个类变为抽象类,C++中的抽象类类似于Java中的接口,抽象类必须至少包含一个纯虚函数。
#includeclass Car { public: virtual void showName() = 0; //纯虚函数 }; class Audi : public Car { public: void showName() override { std::cout << "Audi" << std::endl; } }; class Volvo : public Car { public: void showName() override { std::cout << "Volvo" << std::endl; } }; int main() { Audi audi; Volvo volvo; audi.showName(); //Audi volvo.showName(); //Volvo return 0; }
十四、map
#include#include
Atreus@AtreusdeMacBook-Pro code % make clang++ -c main.cpp -std=c++11 clang++ *.o -o main Atreus@AtreusdeMacBook-Pro code % ./main C:1 C:1 C++:3 Python:6



