用于大型程序工具
异常处理
C++中使用throw
抛异常,异常后面的代码将不会执行,如果没有try-catch
来捕获异常,系统会调用terminate
函数。
try-catch
如果出现异常,系统会在函数中找与try
匹配的catch
,如果没找到则会继续检查外层的catch
。这会触发stack unwinding,即当前函数如果没有catch住,则当前函数栈会被释放,栈里的类对象会触发析构函数,为了确保对象能被正常释放,析构函数不应该抛出不能被他自身处理的异常。换句话说,如果析构函数中需要执行某个可能抛异常的函数,则必须使用try-catch
,确保异常在析构函数内部被处理。
被throw的异常对象位于内存中的一个特殊位置,编译器会确保该对象在catch
中被访问到并正常销毁。如果抛出的对象是一个指向局部变量的指针,则被指向的对象可能已经被销毁。另外,throw对象的类型在编译期就已经确定,如果它是一个指向基类的Base*
指针,在触发异常时,指向的对象是一个子类的对象,则只有基类部分的内存会被保留,这点需要特别注意。
有时一个单独的catch
不能完整的处理异常,此时可以继续将异常抛给外层的catch
catch(some_error& err){
throw; //rethrow the error to the
}
如果要捕获所有异常,可以用catch(...)
,如果有多个catch
语句,则它必须出现在最后的位置
try-catch
和构造函数
如果构造函数会抛异常,我们需要将其置于try-catch
内
template<typename T>
Blob<T>::Blob(std::initializer_list<T> il) try:
data(std::make_shared<std::vector<T>>(il)) {}
catch (const std::bad_alloc &e) {
handle_out_of_memory(e);
}
此时try-catch
既能捕获构造函数的异常,也能捕获成员初始化列表抛出的异常。
noexcept
对编译器来说,如果预先知道某个函数不会抛出异常,则编译器会简化调用该函数的代码,节省code size。C++11可以用noexcept
指定某个函数不会抛异常
void recoup(int) noexcept;
如果是成员函数,noexcept
需要跟在const
以及引用符号后面,而在final
和override
或虚函数=0
之前。如果一个声明了noexcept
的函数内部仍然抛出异常,则系统会直接调用terminate()
。
namespace
定义在namespace {}
中的变量其生命周期和static
变量相同,在程序结束后才会被销毁。注意,定义在namespace {}
中的变量只对所在文件可见,它们彼此独立,即使名字相同,它们也是不同的变量。如果头文件中有变量定义在namespace {}
中,那么包含他们的cpp文件得到的是不同实体。
C语言中用
static
声明静态变量或函数,在C++中等价于namespace{}
new
和delete
当我们执行一条new
表达式时
std::string* sp = new string("value");
delete sp;
实际上执行了三步操作,首先调用operator new
的标准库函数分配空间,其次是调用构造函数为成员赋值,最后返回该对象的指针。delete
则首先调用对象的析构函数,然后调用oeprator delete
释放内存。operator new
和operator delete
可以被重载来自定义内存分配规则。
void* operator new(size_t size) {
if(void* mem = malloc(size)) {
return mem;
} else {
throw bad_alloc();
}
}
void operator delete(void* mem) noexcept {
free(mem);
}
一般情况下,我们可以自定义具有任何参数的operator new
,但是下面这个函数是不能被重载的,它只能被标准库使用
void* operator new(size_t, void*);
Placement new
由于operator new
只负责分配空间而不负责调用构造函数,如果我们要同时自定义new
和构造对象,则需要使用placement new。其形式如下
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] {braced initializer list}
其中place_address
是一个指针,此时placement new允许我们在该指针指向的地址上直接构造对象,而不用分配内存
auto * memory = std::malloc(sizeof(User));
auto* user = ::new (p1) User("john");
// does not allocate memory
// calls: operator new (sizeof(User),p1)
上面代码将内存分配和函数构造分开,placement new接受的指针可以是任何指针,也就是说甚至可以在stack上分配空间。在底层实现上,placement new实际上就是调用了上面提到的 void* operator new(size_t, void*)
函数,它不分配任何内存,只是返回指针。
显式调用析构函数
编译器支持手动调用析构函数,它可以销毁对象,但不能释放内存
string *sp = new string("value");
sp -> ~string(); //memory is not deallocated
std::free(sp); //free memory
RTTI
运行时类型识别(RTTI)的功能由两个运算符实现
typeid
用于返回表达式类型dynamic_cast
用于将基类指针或者引用安全的转换到派生类
typeid
typeid(e)
接受任何表达式,返回的结果是type_info
类对象或其子类。如果e
是引用类型,则返回引用对象的类型,如果e
是数组时,typeid
并不会返回数组元素的类型,而是返回数组类型。对于静态类型,typeid
的结果在编译时即可确定,但是如果e
是一个包含虚函数的子类,则typeid
需要在运行时执行。
如果e
是指针,则typeid(ptr)
返回的是指针类型T*
而不是T
,如果想要返回对象类型,则需要typeid(*ptr)
。我们可以使用线面代码在运行时知道变量类型,类型的名字均是mangled name
struct SomeObj{};
void func(std::string) {}
int main(){
int x[] = {1,2,3};
std::cout<<typeid(x).name()<<std::endl;
std::cout<<typeid(SomeObj).name()<<std::endl;
std::cout<<typeid(func).name()<<std::endl;
}
枚举
C++中可以定义两种枚举: scoped和unscoped enumeration。其中scoped enumeration包含关键字enum class
或者enum struct
enum class trafficLight {
red,
green,
yellow
};
此时,我们定义了一个类型为colors
的枚举类型。如果是unscoped enumeration,关键字class
和枚举类型的名字都是可选的,比如
enum color {red, green, yellow};
enum {red, green, yellow};
scoped枚举的作用域遵循常规作用域的准则,其成员不能被外部直接访问。而对于unscoped的枚举成员,其可以解决枚举成员重名的问题,比如
enum color {red, yellow, green};
enum stoplight { red, yellow, green}
上面代码会报错,由于没有作用域限制,{red, yellow, green}
会被重复定义。而下面代码是OK的,因为枚举变量定义在各自的作用域内
enum color {red, yellow, green};
enum class color {red, yellow, green}
对于unscope的enum成员,它们可以和int
进行隐式转换,而对于有scope的enum则不可以
int i = color::red; //OK
int j = trafficLight::red; //error, scoped enums can't be implictly converted
int z = (int)trafficLight::red; //OK
前置声明
在C++11中,我们可以提前声明enum
,但必须指定其大小
enum intValues: unsigned int; //unscoded, must specify a type
enum class open_modes; // scoped enums can use int by default!
类成员指针
成员指针是指可以指向类的非静态成员的指针。类的静态成员不属于任何对象,指向静态成员的指针和普通的指针没有却别。成员指针包含两部分:类类型和成员的类型,当初始化时,我们令其指向类的某个成员,但不指定该成员所在的object对象,直到使用成员指针时,才提供object。
class Screen {
public:
typedef std::string::size_type pos;
Screen(std::string contents):contents(contents){}
char get_cursor() const { return contents[cursor]; }
char get() const;
char get(pos ht, pos wd) const;
private:
std::string contents;
pos cursor;
pos height, width;
};
数据成员指针
数据成员的指针格式为
const string Screen ::*pData = &Screen::contents;
pData
是一个指向Screen
类contents
成员的指针,和普通的指针不同的是,在*
之前需要添加classname::
。pData
可以指向任何Screen对象的一个成员,不管该Screen对象是否是常量。注意,这里并没有为pData
赋值,该指针并没有指向任何数据。
const string Screen ::*pData = &Screen::contents;
Screen myScreen("screen");
Screen* pScreen = &myScreen;
auto s1 = myScreen.*pData;
auto s2 = pScreen->*pData;
我们可以通过两种方式来访问数据成员指针中的内容 : .*
或者->*
。s1
和s2
的值为字符串"screen"
。需要注意的是,上述代码实际上并不会编译成功,因为contents
是类的私有成员,&Screen::contents
无法直接访问到,此时,我们可以顶一个函数,令其返回值为成员指针
class Screen {
public:
static const std::string Screen::*data() {
return &Screen::contents;
}
}
从右到左阅读返回值类型,data()
返回的是一个指向Screen
类的string
类型成员的const
指针。因此,上面使用pData
的代码可以改为
const string Screen::*pdata = Screen::data();
auto s = myScreen.*pdata;
成员函数指针
与数据成员指针类似,我们也可以顶一个成员函数指针
auto pmf = &Screen::get_cursor;
如果有函数重载