======C++面向对象高级编程(上)第二周======
本页内容是 //Boolan// C++ 开发工程师培训系列的笔记。\\
Mystring s0("Hello"); //Default construct
MyString s1(s2); // construct by copy
s1 = s2; // copy assignment
\\
对于不带指针的类,系统会默认提供一个**按位拷贝**的复制操作来处理类成员之前可能发生的复制操作;这对于线性存储的数据结构来说是没有问题的。但是,如果我们在带指针的数据结构之间使用编译器自带的复制操作(上例),那么就有问题了:我们复制的其实是
inline myString::myString(const char* cstr =0)
{
if (cstr)
{
m_data = new char[strlen(cstr) + 1]; // add one more space to store the string ending sign
strcpy(m_data, cstr);
}
else
{
m_data = new char[1]; //apply a new space for string ending sign
*m_data = '\0';
}
}
有两点需要注意的是:
* ''strlen(cstr) + 1'' 的操作是为了申请一个空间来装载字符串结束的标志。
* ''\0'' 是 C/C++ 中用于描述 //C-Style String// 结束的标志(另外一种表达字符串的形式是指针+空间长度)。
\\
\\
我们注意到这里使用了 ''new'' 来申请空间给 ''m_data'',因此必须在析构函数中写上 ''delete'' 来释放空间:
inline MyString::~MyString()
{
delete[] m_data;
}
===拷贝构造函数===
对于 ''MyString s1(s2)'' 这样的操作,我们用拷贝构造函数来进行处理。我们在这里实际上进行了两步操作:
- 构造一个空的 ''s1'',并为其申请足够容纳下 ''s2'' 的空间。
- 把 ''s2'' 里的内容复制到 ''s1'' 里面去。
\\
实现起来就应该如下了:
inline myString::MyString(const MyString::& str)
{
m_data = new char[ strlen(str.m_data) +1];
strcpy(m_data, str.m_data);
}
拷贝构造函数和前面的一般构造函数写法基本一致;差别在输入对象上。
\\
\\
**2017/04/28补充**:
\\
拷贝操作牵涉到指针的,一定要判断指针指向内容是否为空:
if (leftUp == nullptr);
\\
===拷贝赋值函数===
\\
拷贝赋值函数(''='' 操作符重载)是为了实现 ''s1 = s2'' 此类的操作。基本的拷贝流程如下:
- 检测是否是自我赋值(Self Assignment),如果是,不改变当前的内容。
- 清除 ''s1'' 的内容。
- 根据 ''s2'' 的空间大小来申请足够大的空间给 ''s1''。
- 复制 ''s2'' 到 ''s1''。
值得注意的是,**检测自我赋值在这里有非常重要的意义**。假设用户输入了自我赋值,即 ''s1'' 和 ''s2'' 指向的其实是**同一个字符串**;如果我们跳过第一步检测,直接进行删除 ''s1'' 操作,那么经过删除以后,''s1'' 和 ''s2'' 都不存在了。
\\
\\
检测方法如下:
if (this == & str)
return *this;
===1.4.浅拷贝和深拷贝===
这两者的区别是为什么我们要设计 //Big Three// 的真正原因。\\
\\
**浅拷贝**(//Shallow copy//),又称为 //Field-by-Field Copy//,也正是编译器提供的默认拷贝方法,是一种**按位拷贝**的方法。如果数据结构带引用,那么**这种方法只会拷贝引用**,而不会拷贝引用指向的内容。
\\
\\
**深拷贝**(//Deep Copy//)则是通过新建一个对象,将目标里的内容逐步拷贝到新建对象里的方法( ''strcpy()'' 就是拷贝内容的方法)。
\\
\\
\\
{{ cs:programming:cpp:boolan_cpp:deepcopy.jpg?600 |}}
\\
\\
回到我们的程序,我们可以看到三个操作开头的([[cs:programming:cpp:boolan_cpp:oop_a_week2|见1]])都涉及到了拷贝。假设 ''A'' 和 ''B'' 都是字符串,我们要将 ''B'' 的内容 拷贝给 ''A'',来看一看如果我们直接使用浅拷贝,会造成什么样的后果:
\\
\\
\\
Complex c1(1,2);
Complex c2 = new Complex(c1, c2);
他们的主要区别也就是栈和堆的区别;**栈**(//Stack//)和**堆**(//Heap//)是 C++ 中定义的两种**使用方法不同**的**内存空间**。
===栈 Stack===
栈是系统为调用函数所准备的空间。当函数被调用的时候,系统会自动在**栈顶**为被调用函数保留一个内存区域。这些内存区域主要存放**局部变量**,和一些 //Bookkeeping Data//(用于内存释放,调试等等,见 [[cs:programming:cpp:boolan_cpp:oop_a_week2#2.5.VC 中堆的内存块结构|2.5]])。当函数结束调用时候,系统会自动回收这一部分内存。栈的内存分配策略是 //LIFO// (Last In First Out),也就是最近被调用的函数(**栈顶**的内存区)会被最先释放掉。
===堆 Heap===
堆是系统用于动态分配内存所准备的空间。堆中的空间不会自动分配,也不会自动释放,必须要程序员自己指定(''new'' & ''delete'')。堆的存放结构类似于链表,申请堆空间的时候必须要指定入口指针。
===堆和栈的主要区别===
* 存放的内容不同。栈主要用于存储函数的相关信息(**逻辑**),而堆用于存储相关资料(**数据**)。
* 申请方式不同。栈由系统自动分配,而堆由程序员指派大小和释放。
* 响应方式不同。在栈中,只要栈的空间大于申请空间,系统会为程序持续提供内存。当空间超过上限,产生 //Stack overflow// 的错误。而在堆中,操作系统会提供一个记录空闲地址的链表。当申请指定大小的空间时,系统遍历链表,找出空间大于或者等于申请空间大小的节点,然后将其从链表里删除,同是把空间给用户使用。因此,申请的空间并不一定恰好等于申请的大小。
* 申请大小限制不同。栈是一块连续的内存区域,最大容量是由操作系统事先规划好的。因此,能从栈获得的空间不大。而堆是类似于链表的数据结构,是不连续的。因此,堆获得的空间较灵活,也比栈大。
* 效率不同。栈有系统分配,而且地址连续,因此速度很快。堆由程序员手动分配,速度慢,而且容易产生内存碎片。
===New & Delete===
我们从上面知道**堆**是要通过程序员手动分配的。在 C++ 中,我们分配的内存的方法就是 ''new'' 和 ''delete''。而我们也知道,申请堆空间是一定要指定入口指针的,因此 ''new'' 和 ''delete'' 内部也牵涉到了指针的操作\\
\\
Class *pc = new Class;
而实际上,这个 ''new'' 分成了好几个部分:
\\
\\
**分配内存**:申请 ''class'' 需要的内存空间,这部份通过 C++ 中的 ''operator new'' 函数实现。而在这个过程中, ''operator new'' 会返回一个指向新申请空间的指针;由于不清楚申请空间要装什么,我们用一个 ''void'' 指针来暂时装载(C++ 中 ''void'' 指针可以通过类型转换转换成任何类型的指针)。
void *p = operator new(sizeof(Class));
而通过源码发现, ''operator new'' 实际上是在调用 ''malloc'':
void* p;
while ((p = ::malloc(size)) == 0) {
std::new_handler nh = std::get_new_handler();
}
\\
**转换指针类型**:我们刚才得到的指针 ''p'' 是 ''void'' 类型的。但我们使用的时候是使用的类类型的指针,因此我们需要把这个指针的类型转换掉。
Class *pc = static_cast(p);
**创建对象**:空间申请了,也有了入口指针,接下来就是用构造函数创建一个对象啦。因为这里的指针代表着我们的新对象,所以,我们用指针调用构造函数即可:
pc->Class();
这样我们就通过 ''new'' 手动的申请了一片堆空间,并在这个堆空间中创建了一个类的对象。
==Delete的过程==
''deltete'' 恰好是跟 ''new'' 相反:
\\
\\
**调用析构函数**:先将先前申请空间中的对象用析构函数摧毁掉:
pc->~Class();
**释放内存**:跟 ''new'' 类似,''delete'' 实际上调用了一个 ''operator delete'' 的函数:
operator delete(pc);
而该函数实际上调用了 C 语言中的 ''free()'' 函数。
==避免野指针==
我们用 ''detete'' 将指针指向的空间释放掉了,但指针依旧存在。而此时的指针指向了一片被删掉的空间;这个指针是无效的。如果我们再对此指针进行操作,比如:
delete pointer;
delete pointer;
程序就会报错了。
\\
\\
对于这样的情况,我们应该在析构函数里做出相应的处理:
~dtor
{
pointer = nullptr;
}
在 C++ 中删除 ''nullptr'' 指针是合法的,因此避免了上述问题。
===VC 中堆的内存块结构===
了解了堆的内存块结构,我们就能明白为什么 ''new'' 和 ''delete'' 必须配套使用了。
\\
\\
我们通过 ''new'' 和 ''delete'' 申请或释放的内存块(Block),跟我们在程序里看到的其实不太一样:
\\
\\
static int num;
//Static// 成员属于类,而不属于任何一个对象。 换句话说,//Static// 成员是**唯一**的;因此不能用 ''this'' 指针去调用 //Static// 成员。
\\
\\
注意: //Static// **成员必须在类外定义**(初始化),格式如下:
type Class::name = value;
==静态成员函数==
//Static// 成员不能用 ''this'' 指针指代。因此普通的成员函数无法对其进行操作(需要 ''this'' parameter)。对此,我们使用静态成员函数来对其专门操作。静态成员函数的声明也是以 ''static'' 开头:
static int total;
static int getTotal();
注意:静态成员函数没有 ''this'' 指针 parameter, 因此**只能访问静态成员变量**。
==静态成员函数访问的方式==
\\
用静态成员函数访问静态成员变量有两种方式:
* 通过对象调用
* 通过了类名字调用
通过对象调用的写法如下:
obj.static_func(static_val);
通过类名字的写法如下:
Class::static_func(static_val);
===ostream 对象===
''cout'' 是 C++ 中提供的一个 ''ostream'' 对象,可以输出大部分已知的 build-in 数据类型。之所以能这样,是因为 ''ostream'' 类中重载了大量的 ''<<'' 操作符。
===类模板和函数模板===
模板是一种 C++ 提供的处理公共逻辑的方法,其意义在于减少代码的重复。
\\
\\
模板分为两种:**类模板**和**函数模板**。
\\
\\
类模板的定义如下:
template
class MyClass
{
T val = 0;
....
};
函数模板的声明如下:
template
int func( T& val1, T& val2...)
{
statment......
}
以上定义中的 ''class'' 关键字可以用 ''typename'' 替换。
==类模板和函数模板的区别==
类模板和函数模板最主要区别在于**函数模板可以自行根据** argument **推断** ''T'' **的类型**,而**类模板必须在对象创建的时候手动指定** ''T'' 的**类型**:
Class obj; //using class template, we must specify the type of object
fun(1, 2); //using function temple, T is deduced as int
==class 还是 typename?==
前面提到模板的定义可以使用 ''class'',也可以使用 ''typename''。那到底区别在哪里?\\
\\
这两个关键字区别并不是指类模板和函数模板的差别。就功能性上来说,两个关键字实现的功能基本一样。这里有一个主题是讨论这两个关键字的:[[http://stackoverflow.com/questions/213121/use-class-or-typename-for-template-parameters|链接]],看了一下需要注意的地方如下:
* //Stroustrup// 觉得 ''class'' 有让人误解的可能,于是又写了一个功能相同的 ''typename''。
* 一般情况下,''class'' 用于类模板,而 ''typename'' 用于其他模板(Effective C++ 3rd)
注:C++ 14 规定嵌套模板的类型关键字必须使用 ''class''。
template typename MyTemplate, class Bar> class Foo { }; // error
template class MyTemplate, class Bar> class Foo { }; // good
===命名空间===
命名空间(namespace)可以将一系列的程序功能包装到一个指定的空间内。我们访问这个空间中的数据和功能时需要参考命名空间,从而避免了相关数据和功能命名冲突的可能。
\\
\\
命名空间的定义如下:
namespace name
{
statements...
}
使用命名空间的方法有三种:
* Using directive: 比如 ''using namespace std''。全局声明,使用命名空间内所有内容无需再加上命名空间名。
* Using declaration:比如 ''using std::cout'',对指定的功能声明,使用命名空间内指定功能无需在加上命名空间名。
* 直接使用全名:比如在程序里写 ''std::cout'',每次使用功能必须加上命名空间名。