C++面试 C++11新特性之新类型与初始化

新类型

unsigned long long / long long类型

在C++11中,标准要求long long整型可以在不同平台上有不同的长度,但至少有64位。

我们在写常数字面量时,可以使用LL后缀(或是ll)标识一个long long类型的字面量,而ULL(或ull、Ull、uLL)表示一个unsigned long long类型的字面量。
比如:

//定义了一个有符号的long long变量lli和无符号的unsigned long long变量ulli
long long int lli = -9000000000000000000LL;
unsigned long long int ulli = -9000000000000000000ULL;

事实上,在C++11中,还有很多与long long等价的类型。比如对于有符号的,下面的类型是等价的:long long、signed long long、long long int、signed long long int;unsigned long long和unsigned long long int也是等价的。

与long long整型相关的一共有3个:LLONG_MIN、LLONG_MAX和ULLONG_MIN,它们分别代表了平台上最小的long long值、最大的long long值,以及最大的unsigned long long值。

在我的机器上,long long类型和long类型同样使用64bit空间。


nullptr字面量

C++11标准中专门为空指针增加了nullptr字面量,同时不推荐再使用NULL或者0表示空指针。


constexpr

C++11标准中新增constexpr用于声明常量表达式,编译器会验证此变量的值是否是一个常量表达式。

int out_i = 0; // out_i定义于函数外部
...
constexpr int mf = 20;
constexpr int limit = mf + 1;
constexpr int *p4 = &out_i;
// the following would cause a make error 

// since large is not a constexpr
//constexpr int wrong = large + 1; 

// since &in_j is not a constexpr;
//int in_j = 0;
//constexpr int *p5 = &in_j;

值得注意的是,constexpr指针可以指向非常量变量,只要此变量定义于函数之外,因为这样的变量的指针(地址)是可以在编译期确定的。
另外,下面的constexpr指针与const指针的含义是完全不同的:

constexpr int *p6 = nullptr; // a const pointer point to an int
// p6 = &out_i; // error: p6 is a constexpr
const int *p7 = nullptr; // a pointer point to a const int

第一个指针表示一个常量指针,即指针的值是常量;而第二个指针表示一个指向const int的指针,即指针指向的值是常量。

constexpr还可以用于函数,constexpr函数是指能用于常量表达式的函数,它遵循以下几条约定:

a.返回类型是字面值类型
b.形参类型是字面值类型
c.函数体中必须有且仅有一条return语句

constexpr int sz() { return 42; }
constexpr int new_sz(int cnt) { return sz() * cnt; }
constexpr int size = sz();
constexpr int nsize = new_sz(mf);
//constexpr int wrong_size = new_sz(out_i); // error: out_i is not a constexpr
cout<<"test constexpr: "<<mf<<'\t'<<limit<<'\t'<<p4<<'\t'<<size<<'\t'<<
nsize<<'\t'<<p6<<'\t'<<p7<<endl;

noexcept

noexcept可以用作异常指示符,用于指示一个函数是否会抛出异常。编译器并不检查使用了noexcept的函数是否真的不抛出异常,在运行时,如果一个使用noexcept承诺不抛出异常的函数实际抛出了异常,那么程序会直接终止。

void no_except() noexcept
{
    throw 1;
}
// the following call will cause terminate
//no_except();

noexcept还可以带参数,noexcept(true)表示不会抛出异常,noexcept(false)表示可能抛出异常。
同时noexcept还可以用作运算符,接受一个函数调用,返回一个bool值表示是否会抛出异常。noexcept运算符并不会对其实参进行求值。
将noexcept运算符,结合带参数的noexcept指示符,可以得到如下常用法:

//这种用法表示no_except2和no_except的异常说明保持一致。
void no_except2() noexcept(noexcept(no_except())){}
cout<<"test noexcept: "<<noexcept(no_except())<<'\t'<<noexcept(no_except2())<<endl;

初始化

列表初始化

C++11新标准中为很多类型增加了列表初始化的功能。
可以用列表初始化一个简单变量。

int single_int1 = 0;
int single_int2 = {0};
cout<<"test list initialization:\n"<<single_int1<<'\t'<<single_int2<<endl;

可以用列表初始化一个容器(vector,list,map,set…)。

// vector/list list initialization
vector<string> v1 = {"ab", "cd", "ef"};
list<string> l2{"gh", "ij", "kl"};
//vector<string> v3("mn", "op", "qr"); // wrong initialization format
cout<<"test vector/list list initialization:\n"<<v1[1]<<'\t'<<l2.front()<<endl;

// map/set list initialization
map<string, string> m1 =
{
    {"a", "A"},
    {"b", "B"},
    {"c", "C"}
};
m1.insert({"d", "D"});
set<string> s1 = {"a", "b", "c"};
cout<<"test map/set list initialization:\n"<<m1["d"]<<'\t'<<*s1.find("b")<<endl;

可以在使用new动态分配内存时使用列表初始化。

vector<int> *pv = new vector<int>{0, 1, 2, 3, 4};
int *pi = new int[5]{0, 1, 2, 3, 4};
cout<<"test new alloator using list initialization:\n"<<(*pv)[2]<<'\t'<<pi[2]<<endl;

可以在传入参数/函数返回值时使用列表初始化。

m1.insert({"d", "D"});

vector<string> error_msg(int typ)
{
    switch(typ)
    {
        case 1:
            return {"type1", "msg1"};
        case 2:
            return {"type2", "msg2"};
        default:
            return {};
    }
}

pair<string, string> get_pair(int typ)
{
    if(typ == 1) return {"key", "value"};
    return pair<string, string>("default key", "default value");
}

vector<string> err_msg1 = error_msg(1);
vector<string> err_msg2 = error_msg(2);
vector<string> err_msg3 = error_msg(3);
cout<<"test return value list initialization:\n"
<<err_msg1[1]<<'\t'<<err_msg2[1]<<'\t'<<err_msg3.empty()<<endl;

pair<string, string> p1 = get_pair(1);
pair<string, string> p2 = get_pair(2);
cout<<"test return pair list initialization:\n"<<p1.first<<'\t'<<p2.first<<endl;

类内成员初始化

class InitClass
{
    public:
        void print_class()
        {
            cout<<field1<<'\t'<<field2<<'\t'<<field3<<'\t'<<field4<<endl;
        }
    private:
        int field1 = 1;
        int field2;
        double field3 = 1.0;
        double field4;
};

class InitClassMgr
{
    public:
        vector<InitClass> init_objs = {InitClass()};
};

InitClass test_class;
cout<<"test class member initialization:\n";
test_class.print_class();

InitClassMgr mgr;
cout<<"test class member of class type initialization:\n";
mgr.init_objs[0].print_class();

必须使用初始化列表的几种情况

类对象的构造顺序是这样的:

1.分配内存,调用构造函数时,隐式/显示的初始化各数据成员;

2.进入构造函数后在构造函数中执行一般赋值与计算。

使用初始化列表有两个原因:

原因1.必须这样做:

《C++ Primer》中提到在以下三种情况下需要使用初始化成员列表:

情况一、需要初始化的数据成员是对象的情况(这里包含了继承情况下,通过显示调用父类的构造函数对父类数据成员进行初始化);

情况二、需要初始化const修饰的类成员或初始化引用成员数据;

情况三、子类初始化父类的私有成员

情况一的说明:数据成员是对象,并且这个对象只有含参数的构造函数,没有无参数的构造函数;
如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,如果没有初始化列表,那么他将无法完成第一步,就会报错。

#include <iostream> 
using namespace std;  
class Test  
{  
 public:  
    Test (int, int, int){  
    cout <<"Test" << endl;  
 };  
 private:  
    int x;  
    int y;  
    int z;  
};  
class Mytest   
{  
 public:  
 //初始化列表在构造函数执行前执行
    Mytest():test(1,2,3){       //初始化  
    cout << "Mytest" << endl;  
    };  
private:  
    Test test; //声明  
};  
int _tmain(int argc, _TCHAR* argv[])  
{  
 Mytest test;  
 return 0;  
}  

■情况二的说明:对象引用或者cosnt修饰的数据成员

情况二:当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。

class Test1 
{  
 priate:  
    const int a;             //const成员声明  
 public:  
    Test():a(10){}           //初始化  
};  

class Test2  
{  
 private:  
     int &a;                        //声明  
 public:  
     Test(int a):a(a){}        //初始化  
}  

■情况三的说明:子类初始化父类的私有成员,需要在(并且也只能在)参数初始化列表中显示调用父类的构造函数:如下:

class Test{  
public:  
    Test(){};  
    Test (int x){ int_x = x;};  
    void show(){cout<< int_x << endl;}  
private:  
    int int_x;  
};  
class Mytest:public Test{  
public:  
    Mytest() :Test(110){  
      //Test(110);            
      //  构造函数只能在初始化列表中被显示调用,不能在构造函数内部被显示调用  
    };  
};  
int _tmain(int argc, _TCHAR* argv[])  
{  
 Test *p = new Mytest();  
 p->show();  
 return 0;  
}  

原因2.效率要求这样做:

类对象的构造顺序显示,进入构造函数体后,进行的是计算,是对成员变量的赋值操作,显然,赋值和初始化是不同的,这样就体现出了效率差异,如果不用成员初始化类表,那么类对自己的类成员分别进行的是一次隐式的默认构造函数的调用,和一次赋值操作符的调用,如果是类对象,这样做效率就得不到保障。

关于类对象需要注意的点

构造函数需要初始化的数据成员,不论是否显示的出现在构造函数的成员初始化列表中,都会在该处完成初始化,并且初始化的顺序和其在类中声明时的顺序是一致的,与列表的先后顺序无关,所以要特别注意,保证两者顺序一致才能真正保证其效率和准确性。

为了说明清楚,假设有这样一个类:

class foo
{
private:
    int a, b;
};

foo(){}和foo(int i = 0){}都被认为是默认构造函数,因为后者是默认参数。两者不能同时出现。

② 构造函数列表的初始化方式不是按照列表的顺序,而是按照变量声明的顺序。比如foo里面,a在b之前,那么会先构造a再构造b。所以无论foo():a(b + 1), b(2){}还是foo():b(2),a(b+1){}都不会让a得到期望的值。

class Test
{
public:
//x值依旧是随机值,因为x先于z定义
    Test():z(10), x(z)
    {
        cout << "Test" << endl;
        cout << x << endl;
        cout << z << endl;
    };
    int x;
    int y;
    const int z;
};

③ 构造函数列表能够对const成员初始化。比如foo里面有一个int const c;则foo(int x) : c(x){}可以让c值赋给x。

不过需要注意的是,c必须在每个构造函数(如果有多个)都有值。

④ 在继承里面,只有初始化列表可以构造父类的private成员(通过显示调用父类的构造函数)。

class child : public foo
{};

foo里面的构造函数是这样写的:

foo (int x)
{
  a =x;
};
//通过不了编译的
child(int x)
{ 
    foo(x);
}
child(int x) : foo(x)
{}

总结

long long类型。
nullptr字面量用于表示空指针。
constexpr用于表示常量表达式。
noexcept可以用于指示一个函数是否会抛出异常,同时可以用作运算符判定一个函数是否承诺不抛出异常。
新增基础类型、容器类型、new分配内存时的列表初始化。构建临时变量时也可以直接使用列表初始化。
可以直接对类内成员进行初始化/列表初始化。

猜你喜欢

转载自blog.csdn.net/qq_23225317/article/details/79772309