本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录前一修订版后一修订版 | 前一修订版 | ||
cs:programming:cpp:boolan_cpp:oop_a_week2 [2024/01/14 13:46] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | cs:programming:cpp:boolan_cpp:oop_a_week2 [2024/01/14 13:47] (当前版本) – ↷ 链接因页面移动而自动修正 codinghare | ||
---|---|---|---|
行 1: | 行 1: | ||
+ | ======C++面向对象高级编程(上)第二周====== | ||
+ | 本页内容是 //Boolan// C++ 开发工程师培训系列的笔记。\\ | ||
+ | <wrap em> | ||
+ | ---- | ||
+ | ====1.带指针的类的设计==== | ||
+ | |||
+ | //<wrap em> | ||
+ | \\ | ||
+ | \\ | ||
+ | 我们知道,带指针的类一般用于一些需要动态内存分配的数据。比如处理变长的字符串,我们不可能定义一个数组去装:要么空间浪费,要么根本就不够大。因此,我们常用的实现方式是使用**指针+动态内存申请**(比如链表)这样的数据结构来处理前面提到的数据。这样的好处在于灵活,但反过来,这也对类的操作函数提出了更加严格的要求。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 为什么会有额外的要求?我们可以先来看一下对上文提到的数据结构可能产生的操作(假设类名为 '' | ||
+ | <code cpp linenums: | ||
+ | Mystring s0(" | ||
+ | MyString s1(s2); // construct by copy | ||
+ | s1 = s2; // copy assignment | ||
+ | </ | ||
+ | \\ | ||
+ | 对于不带指针的类,系统会默认提供一个**按位拷贝**的复制操作来处理类成员之前可能发生的复制操作;这对于线性存储的数据结构来说是没有问题的。但是,如果我们在带指针的数据结构之间使用编译器自带的复制操作(上例),那么就有问题了:我们复制的其实是< | ||
+ | \\ | ||
+ | \\ | ||
+ | 因此,我们在设计带指针的类的时候,需要额外考虑三个内容(// | ||
+ | \\ | ||
+ | \\ | ||
+ | 特别需要提到的是,在带指针的类中,我们使用的数据结构是动态的申请内存空间的;当这些空间使用完毕之后,我们需要手动的去删除这些空间。编译器默认提供的析构函数无法处理这样的请求,因此我们必须自定义析构函数来确保这些空间的释放,避免造成内存泄漏。相关的 C++ 关键字是:'' | ||
+ | |||
+ | ===一般构造函数=== | ||
+ | |||
+ | 为了实现用字符串直接初始化类对象,我们需要设计一个一般的构造函数来处理这些事情。而需要注意的是用于初始化的字符串可能为空;我们分两种情况来处理: | ||
+ | * '' | ||
+ | * '' | ||
+ | <code cpp linenums: | ||
+ | inline myString:: | ||
+ | { | ||
+ | if (cstr) | ||
+ | { | ||
+ | m_data = new char[strlen(cstr) + 1]; // add one more space to store the string ending sign | ||
+ | strcpy(m_data, | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | m_data = new char[1]; //apply a new space for string ending sign | ||
+ | *m_data = ' | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | 有两点需要注意的是: | ||
+ | * '' | ||
+ | * '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 我们注意到这里使用了 '' | ||
+ | <code cpp linenums: | ||
+ | inline MyString:: | ||
+ | { | ||
+ | delete[] m_data; | ||
+ | } | ||
+ | </ | ||
+ | ===拷贝构造函数=== | ||
+ | |||
+ | 对于 '' | ||
+ | - 构造一个空的 '' | ||
+ | - 把 '' | ||
+ | \\ | ||
+ | 实现起来就应该如下了: | ||
+ | <code cpp linenums: | ||
+ | inline myString:: | ||
+ | { | ||
+ | m_data = new char[ strlen(str.m_data) +1]; | ||
+ | strcpy(m_data, | ||
+ | } | ||
+ | </ | ||
+ | 拷贝构造函数和前面的一般构造函数写法基本一致;差别在输入对象上。 | ||
+ | \\ | ||
+ | \\ | ||
+ | **2017/ | ||
+ | \\ | ||
+ | 拷贝操作牵涉到指针的,一定要判断指针指向内容是否为空: | ||
+ | <code cpp> | ||
+ | if (leftUp == nullptr); | ||
+ | </ | ||
+ | |||
+ | \\ | ||
+ | ===拷贝赋值函数=== | ||
+ | \\ | ||
+ | 拷贝赋值函数('' | ||
+ | - 检测是否是自我赋值(Self Assignment),如果是,不改变当前的内容。 | ||
+ | - 清除 '' | ||
+ | - 根据 '' | ||
+ | - 复制 '' | ||
+ | 值得注意的是,**检测自我赋值在这里有非常重要的意义**。假设用户输入了自我赋值,即 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 检测方法如下: | ||
+ | <code cpp linenums: | ||
+ | if (this == & str) | ||
+ | return *this; | ||
+ | </ | ||
+ | |||
+ | ===1.4.浅拷贝和深拷贝=== | ||
+ | |||
+ | 这两者的区别是为什么我们要设计 //Big Three// 的真正原因。\\ | ||
+ | \\ | ||
+ | **浅拷贝**(// | ||
+ | \\ | ||
+ | \\ | ||
+ | **深拷贝**(// | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | {{ cs: | ||
+ | \\ | ||
+ | \\ | ||
+ | 回到我们的程序,我们可以看到三个操作开头的([[cs: | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | 看到了吧!如果使用浅拷贝,会导致拷贝和被拷贝的对象的指针同时指向拷贝的对象,而被拷贝的对象内容则失去了指针,从而造成了被拷贝对象内存泄漏。\\ | ||
+ | |||
+ | ====内存管理==== | ||
+ | |||
+ | 我们可以考虑下以下两个语句有什么不同: | ||
+ | <code cpp linenums: | ||
+ | Complex c1(1,2); | ||
+ | Complex c2 = new Complex(c1, c2); | ||
+ | </ | ||
+ | 他们的主要区别也就是栈和堆的区别;**栈**(// | ||
+ | |||
+ | ===栈 Stack=== | ||
+ | |||
+ | 栈是系统为调用函数所准备的空间。当函数被调用的时候,系统会自动在**栈顶**为被调用函数保留一个内存区域。这些内存区域主要存放**局部变量**,和一些 // | ||
+ | |||
+ | ===堆 Heap=== | ||
+ | |||
+ | 堆是系统用于动态分配内存所准备的空间。堆中的空间不会自动分配,也不会自动释放,必须要程序员自己指定('' | ||
+ | |||
+ | ===堆和栈的主要区别=== | ||
+ | |||
+ | * 存放的内容不同。栈主要用于存储函数的相关信息(**逻辑**),而堆用于存储相关资料(**数据**)。 | ||
+ | * 申请方式不同。栈由系统自动分配,而堆由程序员指派大小和释放。 | ||
+ | * 响应方式不同。在栈中,只要栈的空间大于申请空间,系统会为程序持续提供内存。当空间超过上限,产生 //Stack overflow// 的错误。而在堆中,操作系统会提供一个记录空闲地址的链表。当申请指定大小的空间时,系统遍历链表,找出空间大于或者等于申请空间大小的节点,然后将其从链表里删除,同是把空间给用户使用。因此,申请的空间并不一定恰好等于申请的大小。 | ||
+ | * 申请大小限制不同。栈是一块连续的内存区域,最大容量是由操作系统事先规划好的。因此,能从栈获得的空间不大。而堆是类似于链表的数据结构,是不连续的。因此,堆获得的空间较灵活,也比栈大。 | ||
+ | * 效率不同。栈有系统分配,而且地址连续,因此速度很快。堆由程序员手动分配,速度慢,而且容易产生内存碎片。 | ||
+ | |||
+ | ===New & Delete=== | ||
+ | |||
+ | 我们从上面知道**堆**是要通过程序员手动分配的。在 C++ 中,我们分配的内存的方法就是 '' | ||
+ | \\ | ||
+ | |||
+ | <WRAP center round important 100%> | ||
+ | New 和 Delete / New[] 和 Delete[] **必须成对出现**! | ||
+ | </ | ||
+ | |||
+ | </ | ||
+ | |||
+ | ==New的过程== | ||
+ | |||
+ | 我们通过 '' | ||
+ | <code cpp> | ||
+ | Class *pc = new Class; | ||
+ | </ | ||
+ | 而实际上,这个 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | **分配内存**:申请 '' | ||
+ | <code cpp> | ||
+ | void *p = operator new(sizeof(Class)); | ||
+ | </ | ||
+ | 而通过源码发现, '' | ||
+ | <code cpp linenums: | ||
+ | void* p; | ||
+ | while ((p = :: | ||
+ | std:: | ||
+ | } | ||
+ | </ | ||
+ | \\ | ||
+ | **转换指针类型**:我们刚才得到的指针 '' | ||
+ | <code cpp linenums: | ||
+ | Class *pc = static_cast< | ||
+ | </ | ||
+ | **创建对象**:空间申请了,也有了入口指针,接下来就是用构造函数创建一个对象啦。因为这里的指针代表着我们的新对象,所以,我们用指针调用构造函数即可: | ||
+ | <code cpp> | ||
+ | pc-> | ||
+ | </ | ||
+ | 这样我们就通过 '' | ||
+ | |||
+ | ==Delete的过程== | ||
+ | |||
+ | '' | ||
+ | \\ | ||
+ | \\ | ||
+ | **调用析构函数**:先将先前申请空间中的对象用析构函数摧毁掉: | ||
+ | <code cpp> | ||
+ | pc-> | ||
+ | </ | ||
+ | **释放内存**:跟 '' | ||
+ | <code cpp> | ||
+ | operator delete(pc); | ||
+ | </ | ||
+ | 而该函数实际上调用了 C 语言中的 '' | ||
+ | |||
+ | ==避免野指针== | ||
+ | |||
+ | 我们用 '' | ||
+ | <code cpp> | ||
+ | delete pointer; | ||
+ | delete pointer; | ||
+ | </ | ||
+ | 程序就会报错了。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 对于这样的情况,我们应该在析构函数里做出相应的处理: | ||
+ | <code cpp> | ||
+ | ~dtor | ||
+ | { | ||
+ | pointer = nullptr; | ||
+ | } | ||
+ | </ | ||
+ | 在 C++ 中删除 '' | ||
+ | |||
+ | ===VC 中堆的内存块结构=== | ||
+ | |||
+ | 了解了堆的内存块结构,我们就能明白为什么 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 我们通过 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 如图中所示可以看到,我们认为(看到)的对象内容,在内存块里实际只占绿色的部分。内存块里还有其他的内容: | ||
+ | * // | ||
+ | * // | ||
+ | * // | ||
+ | * // | ||
+ | ==VC 中堆的数组内存块结构== | ||
+ | |||
+ | 数组在堆内存块中的表现形式和普通内存块大致相似,但多了一个区域用于记录数组的个数(**如下图蓝色区域**): | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 这一点对于开头说到的 '' | ||
+ | |||
+ | ===为什么 New 和 Delete 必须成对出现=== | ||
+ | |||
+ | 必须要提出的是,'' | ||
+ | \\ | ||
+ | \\ | ||
+ | 堆的内存必须通过手动申请创建,手动申请释放。如果只用 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 同时,'' | ||
+ | \\ | ||
+ | \\ | ||
+ | 在数组版本中,每一个数组的元素作为单独的对象出现;而 | ||
+ | \\ | ||
+ | \\ | ||
+ | 根据上一节的图,我们不难想到,构造/ | ||
+ | 试想如果一个 '' | ||
+ | |||
+ | ====补充内容==== | ||
+ | |||
+ | ===Static=== | ||
+ | |||
+ | 有时候我们希望定义一些不同对象需要共用的数据:比如对于不同的银行客户来说,银行的存款利率总是一样的。如果我们把这种数据实例化,那么浪费的空间是很大的。而通过 //Static// ,我们可以达到各个对象共享单个数据的目的。 | ||
+ | \\ | ||
+ | \\ | ||
+ | //Static// 成员变量以 '' | ||
+ | <code cpp linenums> | ||
+ | static int num; | ||
+ | </ | ||
+ | //Static// 成员属于类,而不属于任何一个对象。 换句话说,// | ||
+ | \\ | ||
+ | \\ | ||
+ | 注意: //Static// **成员必须在类外定义**(初始化),格式如下: | ||
+ | <code cpp linenums> | ||
+ | type Class::name = value; | ||
+ | </ | ||
+ | |||
+ | ==静态成员函数== | ||
+ | |||
+ | //Static// 成员不能用 '' | ||
+ | <code cpp linenum: | ||
+ | static int total; | ||
+ | static int getTotal(); | ||
+ | </ | ||
+ | 注意:静态成员函数没有 '' | ||
+ | ==静态成员函数访问的方式== | ||
+ | \\ | ||
+ | 用静态成员函数访问静态成员变量有两种方式: | ||
+ | * 通过对象调用 | ||
+ | * 通过了类名字调用 | ||
+ | 通过对象调用的写法如下: | ||
+ | <code cpp linenums> | ||
+ | obj.static_func(static_val); | ||
+ | </ | ||
+ | 通过类名字的写法如下: | ||
+ | <code cpp linenums> | ||
+ | Class:: | ||
+ | </ | ||
+ | ===ostream 对象=== | ||
+ | |||
+ | '' | ||
+ | |||
+ | ===类模板和函数模板=== | ||
+ | |||
+ | 模板是一种 C++ 提供的处理公共逻辑的方法,其意义在于减少代码的重复。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 模板分为两种:**类模板**和**函数模板**。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 类模板的定义如下: | ||
+ | <code cpp> | ||
+ | template< | ||
+ | class MyClass | ||
+ | { | ||
+ | T val = 0; | ||
+ | .... | ||
+ | }; | ||
+ | </ | ||
+ | 函数模板的声明如下: | ||
+ | <code cpp> | ||
+ | template< | ||
+ | int func( T& val1, T& val2...) | ||
+ | { | ||
+ | statment...... | ||
+ | } | ||
+ | </ | ||
+ | 以上定义中的 '' | ||
+ | |||
+ | ==类模板和函数模板的区别== | ||
+ | |||
+ | 类模板和函数模板最主要区别在于**函数模板可以自行根据** argument **推断** '' | ||
+ | <code cpp> | ||
+ | Class< | ||
+ | fun(1, 2); //using function temple, T is deduced as int | ||
+ | </ | ||
+ | ==class 还是 typename?== | ||
+ | |||
+ | 前面提到模板的定义可以使用 '' | ||
+ | \\ | ||
+ | 这两个关键字区别并不是指类模板和函数模板的差别。就功能性上来说,两个关键字实现的功能基本一样。这里有一个主题是讨论这两个关键字的:[[http:// | ||
+ | * // | ||
+ | * 一般情况下,'' | ||
+ | 注:C++ 14 规定嵌套模板的类型关键字必须使用 '' | ||
+ | <code cpp> | ||
+ | template< | ||
+ | template< | ||
+ | </ | ||
+ | |||
+ | ===命名空间=== | ||
+ | |||
+ | 命名空间(namespace)可以将一系列的程序功能包装到一个指定的空间内。我们访问这个空间中的数据和功能时需要参考命名空间,从而避免了相关数据和功能命名冲突的可能。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 命名空间的定义如下: | ||
+ | <code cpp> | ||
+ | namespace name | ||
+ | { | ||
+ | | ||
+ | } | ||
+ | </ | ||
+ | 使用命名空间的方法有三种: | ||
+ | * Using directive: 比如 '' | ||
+ | * Using declaration:比如 '' | ||
+ | * 直接使用全名:比如在程序里写 '' | ||
+ | <WRAP center round alert 100%> | ||
+ | 不要在头文件里面写 using namespace std,非常容易造成命名空间污染。 | ||
+ | </ |