本系列为根据侯捷老师系列课程所编写的笔记。
1. 三大函数:拷贝构造,拷贝复制,析构函数
class String
{
public:
String(const char* cstr = 0);
String(const String& str);//拷贝构造
String& operator = (const String& str)
~String();
char* get_c_str() const {return m_data;};
private:
char* m_data;
};
(1)拷贝构造
普通的构造函数:
inline String::String(const char* cstr = 0)
{
if(cstr)
{
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}else
{
m_data = new char[1];
*m_data = '\0';
}
}
深拷贝和浅拷贝的区别:
浅拷贝:仅将指针进行了拷贝;
深拷贝:将指针指向的内容进行拷贝。
拷贝构造函数(深拷贝):
inline String::String(const String& str)
{
m_data = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
}
(2)拷贝赋值
步骤:
1.销毁原先所存储的值;
2.创建一个与赋值变量大小相同的空间;
3.将变量的值拷贝进来;
4.返回指向自身的*this。
inline String& String::operator = (const String& str)
{
if(this == &str)//自我赋值
{
return *this;
}
delete[] m_data;
m_data = new char[strlen(str.m_data) + 1];
strcpy(m_data, str.m_data);
return *this;
}
(3)析构函数
析构函数调用时机:
1.变量在离开其作用域时被销毁;
2.当一个对象被销毁时,其成员被销毁;
3.容器被销毁时,其元素被销毁;
4.对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁;
5.对于临时对象,当创建它的完整表达式结束时被销毁。
inline String::~String()
{
delete[] m_data;
}
2.堆、栈与内存管理
栈:存在于某作用域的一块内存空间。例如调用函数时,函数本身会形成一个stack用来放置它所接收的参数,以及返回地址。
stack object的生命周期:栈中对象的在作用域结束时就会被销毁(但有种例外,如果对象被声明为static,那么其直到程序结束后才会被销毁)。
堆:由操作系统提供的一块全局的内存空间,程序可动态分配从中获取若干区块。
heap object的生命周期:其生命周期在指针被delete之际结束。
new
new:先分配内存,再调用构造函数
Complex* pc = new Complex(1, 2);
被转换为
void* mem = operator new(sizeof(Complex));//分配内存,内部调用malloc
pc = static_cast<Complex*>(mem);//把空类型指针转换为目标类型指针
pc->Complex::Complex(1, 2);//构造函数
delete
delete:先调用析构函数,再释放内存
String* ps= new String("Hello");
delete ps;
转换为
String::~String(ps);//析构函数
operator delete(ps);//释放内存,内部调用free(ps)
动态分配的内存块
一个Complex类的内存分配
在每个区块上头放个cookie,记录分配内存块的长度。
array new所分配的内存必须搭配array delete销毁。
(适用于带有指针的类,但不带指针的类用这个方法也是个好习惯)
m_data = new char[10];
delete[] m_data;
若不搭配array delete,则只会调用一次析构函数,即只删除了第一个元素。
String* p = new String[3];
delete[] p;//调用三次析构函数
String* p = new String[3];
delete p;//调用一次析构函数
3.补充知识
静态static
非静态函数通过this指针来判断当前调用该函数的对象是哪个
静态函数和静态数据在程序里只有一份
静态数据:当所有类的实例都用同一个数据时,可使用静态数据
静态函数:没有this指针,只能存取静态数据
class Account
{
public:
static double m_rate;
static void set_rate(const double& x){m_rate = x;}
};
double Account::m_rate = 8.0;
int main()
{
Account::set_rate(5.0);//调用static函数的方法一:通过类名调用
Account a;
a.set_rate(7.0)//调用static函数的方法一,通过对象调用
}
把构造函数放在private区
class A
{
public:
static A& getInstance();
setup(){...}
private:
A();
A(const A& rhs);
...
};
A& A::getInstance()
{
static A a;
return a;
}
template 模版
在编译时替换其中的类型
函数模版:
template <class T>
inline const T& min(const T& a, const T& b)
{
return b < a ? b : a;
}
编译器会对函数模版进行实参推导,不必明确指出需要的变量类型。
namespace
将所有东西包装进一个namespace中,可防止有重名的现象。
组合与继承
Composition(复合),表示has-a
template <class T>
class queue
{
...
protected:
deque<T> c;
}
queue中有一个deque,这就是复合,表示一个中含有另一个。
queue内部使用deque来实现,将deque改造成queue,所以queue就是一个Adapter(适配器),完成一些特定的任务。
复合关系下的构造和析构
构造由内而外:要先把内部的Component构造完毕后(编译器自动执行),再调用自己的构造函数。
析构由外而内:外部先把析构函数调用后,再调用内部Component的析构函数。
就和穿衣服一样,要先把内部的衣服穿好了才能穿外部的衣服,反之,要先把外部的衣服脱了才能脱内部的衣服。
委托(delegation),使用reference的复合
class String
{
public:
...
private:
StringRep* rep;
};
class StringRep
{
friend class String;
StringRep(const char* s);
~StringRep();
int count;
char* rep;
};
和复合差不多,但是委托是使用指针将另一个类包含在其中,所以相当于是一个比较“虚”的复合。
继承(Inheritance),表示is-a
class _List_node_base
{
_List_node_base* _M_next;
_List_node_base* _M_prev;
};
template <typename _Tp>
class _List_node : public _List_node_base
{
_Tp _M_data;
};
public继承,表示派生类是一种基类,父类数据可以完全继承下来(就好像继承传家宝一样)
继承和复合类似,也是派生类中包含着一个父类,所以其构造和析构和复合一致:
构造由内而外:先调用基类的默认构造函数,再调用自身的构造函数;
析构由外而内:先调用自身的析构函数,在调用基类的构造函数。
基类的析构函数必须是virtual,否则会出现undefined behavior
继承中的虚函数
派生类继承的是父类中函数的调用权。
非虚函数:不希望派生类重新定义(override,重写)它;
虚函数:希望派生类重新定义(重写)它,且对它已有默认定义;
纯虚函数:希望派生类重新定义(重写)它,但对它没有默认定义。
class Shape
{
public:
virtual void draw() const = 0;//纯虚函数
virtual void error(const std::string& msg);//虚函数
int objectID() const;//非虚函数
}
在调用函数的时候,是使用this指针进行的,所以派生类的指针能找到相对应的函数。
复合+继承关系下的构造和析构
构造:先构造基类,再构造派生类,所以先调用基类的构造函数,再调用复合类的构造函数。