用于大型程序工具


异常处理

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以及引用符号后面,而在finaloverride或虚函数=0之前。如果一个声明了noexcept的函数内部仍然抛出异常,则系统会直接调用terminate()

namespace

定义在namespace {}中的变量其生命周期和static变量相同,在程序结束后才会被销毁。注意,定义在namespace {}中的变量只对所在文件可见,它们彼此独立,即使名字相同,它们也是不同的变量。如果头文件中有变量定义在namespace {}中,那么包含他们的cpp文件得到的是不同实体。

C语言中用static声明静态变量或函数,在C++中等价于namespace{}

newdelete

当我们执行一条new表达式时

std::string* sp = new string("value");
delete sp;

实际上执行了三步操作,首先调用operator new的标准库函数分配空间,其次是调用构造函数为成员赋值,最后返回该对象的指针。delete则首先调用对象的析构函数,然后调用oeprator delete释放内存。operator newoperator 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是一个指向Screencontents成员的指针,和普通的指针不同的是,在*之前需要添加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;

我们可以通过两种方式来访问数据成员指针中的内容 : .*或者->*s1s2的值为字符串"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;

如果有函数重载

Resources