【C++ Server】四、C++语言

四、C++语言
1、下面这种情况会由于[]的使用而类A没有默认构造函数而编译不通过。
#include <QCoreApplication>
#include <QDebug>
class A
{
public:
    A(int nValue) : m_nValue(nValue)
    {

    }

private:
    int m_nValue;
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    std::map<int, A> mapTmp;
    //mapTmp[1] = A(1);     编译不通过
    mapTmp.insert({1, A(1)});

    return a.exec();
}

2、注意无符号整数和有符号整数的比较。
有符号整数会转换为无符号的整数,比如-1。

std::size_t unsignedIntValue = 10;
int signedIntValue = -1;
if(signedIntValue > unsignedIntValue)
    qDebug() << "-1>10";        // 输出
else
    qDebug() << "-1<=10";   

建议使用有符号的整数。    

3、int nRet = func(new classA(), new classB());
这种写法是不安全的,因为有可能先给classA分配资源,然后再给classB分配资源,
最后才调用classA的构造函数和classB的构造函数。
如果在调用classA构造函数的过程中崩溃了,那么分配给classA的资源可以被释放掉,但是分配给classB的资源就没法释放了。

4、
void funcTest1(int* pValue)
{
    pValue = new int(1);
}
void funcTest2(int* &pValue)
{
    pValue = new int(1);
}
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    int* pValue = nullptr;
    funcTest1(pValue);
    qDebug() << "pValue1: " <<pValue;
    funcTest2(pValue);
    qDebug() << "pValue2: " <<pValue;

    return a.exec();
}

5、C++只有两种main函数的声明:
int main()
int main(int argc, char** argv)

int main()
{
    //return 0;如果没有写,编译器会自动地加上exit(0)
}

6、关键字alignas和alignof。一个结构体如果不指明对齐方式,则是默认以最大的成员的字节数作为对齐,如果结构体为空,则是1。
struct alignas(1) T_MyDev
{
    double d;
};

T_MyDev是一字节对齐。

alignof(T_MyDev)的值是8,alignas(1)不生效。

7、关键字and与&&的含义用法是一样的。
if(1==a && 2==b)
if(1==a and 2==b)

8、尽量不要将bool类型作为函数的参数,取代的方法是可以使用枚举。

9、在linux平台下,char是没有正负的unsigned char 。在windows平台下是有正负的signed char。

10、constexpr修饰的函数在编译期如果能算出结果就直接算出来了,如果不能直接算出来就退化成一个普通函数。
因此一个数组用constexpr修饰的函数算出的大小作为数组的大小是可以的。

11、const_cast打破const属性。但用到const_cast的代码大多不好,因为打破了一开始的承诺。

12、decltype关键字的用法(比较少用,建议用auto)。

struct T_MyDev
{
    double d;
};
const T_MyDev* pDev = new T_MyDev{0};
auto d1 = pDev->d;                // d1是double类型
decltype(pDev->d) d2;            // d2是double类型
auto&& d3 = d2;                 // d3是d2的引用
decltype((pDev->d)) d4 = d2;    // d4是d2的引用.多了个括号,意义就不一样了.

template<typename T, typename U>
auto add(T a, U b) -> decltype(a+b)    // C++11,后置返回类型,根据传入的参数来决定返回类型
{
    return a+b;
}

template<typename T, typename U>
auto add(T a, U b)    // C++14,根据传入的参数来决定返回类型.但是有坑,因为最后的结果有没有括号会影响返回类型.
{
    return a+b;
}

13、dynamic_cast:把父类指针转换为子类指针。对指针和引用有着不同的处理方式。

Base* b1 = new Derived();
if(Derived* d1 = dynamic_cast<Derived*>(b1))    // true,如果转换失败的话会返回一个空指针.
{
    std::cout << "cast success";
    d1->name();
}

Base b2;
Derived& d2 = dynamic_cast<Derived&>(b2);    // 如果转换失败的话会抛出异常。因为引用没法指向空的,因为失败了的话不知道怎么处理.

14、enum加上class可以避免命名冲突。当然也可以用命名空间。
enum ColorOldWay{ Red, Green, Blue};
ColorOldWay r = Red;
int a = r;

enum class ColorNewWay{ Red, Green, Blue};
ColorNewWay r = ColorNewWay::Red;
int a = static_cast<int>(r);

15、能加explicit的地方就要加explicit。

16、在一个类A中修饰一个函数或者一个类为friend,则这个函数或这个类允许访问这个类A的私有成员。
但如果一个类B继承了类A,这个函数或这个类却不是B的友元。

17、如果一个函数的确不会抛出异常的时候,可以修饰为noexcept。编译器会对这样的函数优化,高效地执行。
void func() noexcept
{
    ...
}
但最好不要用这个关键字,因为没法100%保证自己写的函数没有抛出异常。万一这个函数最后抛出了异常,则程序会直接崩溃掉。

18、nullptr其实是一种类型。

19、reinterpret_cast、static_cast、const_cast都可以用C语言类型的转换来转。但是建议使用C++语言类型的转换。

20、sizeof是编译期就算出来了。

21、static局部变量在多线程环境下是不安全的。

22、static_assert:编译期间就发挥作用,能在编译期发现的问题最好在编译期发现。
assert:一个宏定义,在发布版本和debug版本的表现是不一样的,在运行期间发挥作用。

23、static_cast:同种类型的转换。不影响运行效率。

24、有些typedef的功能可以用using来实现。建议用using。
typedef std::map<int, int> Group;
using Group = std::map<int, int>;

25、volatile:告诉编译器对该变量不要进行优化。一般用不上。

26、用类前置声明的时候,接下来的那个类只能用指针形式的,因为编译器此时不知道前置声明类的大小,此时前置声明类是不完整类型。此时也不需要include该前置声明类的头文件。但是如果这个类有需要调用该前置声明类里面函数的函数Func,则应该在这个类的cpp文件中include该前置声明类的头文件,并实现函数Func。

头文件里面应当尽可能地少include头文件,尽可能地使用前置声明,否则可能会导致编译慢。

当一个类需要知道用到的那个类的大小的时候,就不能仅仅用前置声明,而应该包含该类的头文件。

27、如果一个类没有写拷贝构造函数和拷贝赋值函数的时候,没有写的原因是不需要,那么应该明确地告诉编译器(声明为delete)。

28、对于右值来说,是不能取地址的。对于左值来说,是可以取地址的。

29、右值引用。
int& a1 = 2;        // 2是右值,右值是不能取地址的。错误写法。
int&& a2 = 2;         // a2是右值引用。正确写法。

const int& a3 = 2;  // 正确写法.
a2 = 30;            // 对右值引用重新赋值,正确写法。

int b1 = 10;
int&& b2 = b1;                    // 错误写法.
const int&& b3 = b1;            // 错误写法.
int&& b3 = std::move(b1);        // 正确写法.
const int&& b4 = std::move(b1);    // 正确写法.

std::vector<int> func()
{
    std::vector<int> vecTmp(100);
    return vecTmp;
}
auto vec = func();

30、MyClass里面有拷贝构造函数,注意参数一定要加const。
class MyClass
{
public:
    ~MyClass(){
        if(nullptr!=m_pValue){
            delete m_pValue;
            m_pValue = nullptr;
        }
    }
    MyClass(const MyClass& rhs){
        m_pValue = new int(*(rhs.m_pValue));
    }
    ...
    MyClass(MyClass&& rhs){            // 也就是重载拷贝构造函数.
        m_pValue = rhs.m_pValue;
        rhs.m_pValue = nullptr;
    }
    MyClass& operator=(MyClass&& rhs){
        if(&rhs == this)    return *this;    //移动赋值函数要考虑自赋值的情况.
        delete m_pValue;
        m_pValue = rhs.m_pValue;
        rhs.m_pValue = nullptr;
        return *this;
    }
    
private:
    int* m_pValue;
}
MyClass a;            // a是一个左值。其实右值一切特征,左值都有。
MyClass b = std::move(a);    // 告诉编译器把a变成一个右值。此时将调用MyClass的拷贝构造函数,就算std::move(a)返回的是MyClass&&,但是可以转换为const的引用,注意不可以转换为引用。也就是说我们可以取临时变量的const引用,才不能取引用。

其实当MyClass没有定义移动构造函数的时候,
MyClass b = a;这种写法和上面那种是一样的。也就是说,如果没有定义MyClass(MyClass&& rhs)这个函数,那么std::move的作用就退化了。

std::move的含义就是把右边临时对象的一些资源直接移到左边来,而不用重新去new。
但像上面的std::move(a);之后,就不能再使用a了,因为a的资源都移交给b了。

31、编译器默认生成的析构函数是inline的。这样子会让析构函数进行代码展开。我们可以自己声明析构函数,然后在cpp文件实现,从而阻止这种行为。

绝对不要在析构函数里面抛出异常。编译器会自动地把析构函数修饰为noexcept。

可以明确地告诉编译器我们写的析构函数会抛出异常。这样的话,调用析构函数的地方就能加try-catch进行捕获。
~MyClass() noexcept(false){
    throw std::string("error");
}

void func(){
    try{
        MyClass m1;
        //MyClass m2;
    }
    catch(...){
        ...
    }
}
但如果MyClass父类的析构函数也noexcept(false),并且抛出了异常,那么程序还是会崩溃掉。又或者说定义了变量m2,程序还是会崩溃掉。又或者说MyClass里面new了一个资源,而这个资源delete的位置在析构函数的throw之后,那么这个资源不会被delete掉。所以绝对不要在析构函数里面抛出异常,因为程序处理不了同时抛出两个异常的情况。

32、调用std::swap函数也不要抛出异常。析构函数如果不抛出异常,swap函数基本也不会抛出异常。

33、构造函数失败就应该抛出异常。

34、一个类如果有可能作为基类,则它的析构函数应该被修饰为virtual。因为当父类指针指向子类对象的时候,delete父类指针,父类析构函数是virtual的,才能调用到子类的析构函数。

但如果一个类不会作为基类的时候(这个类如果没有定义虚成员函数,那么这个类就算作为基类,也是和普通的类是一样的,没有必要把析构函数修饰为virtual),就不要把析构函数修饰为virtual,否则效率会变慢。

如果当父类指针指向子类对象的时候,父类指针是不能调用子类有而父类没有的那些函数的。如果我们明确地知道这个父类指针是指向某个子类对象,则可以通过以下三种方式去转换为子类指针,从而能够调用子类的函数:
Derived* d = static_cast<Derived*>(pBase); // 不安全。如果pBase不是指向Derived对象的,则后面调用到Derived的函数的时候可能会崩溃。
Derived* d = dynamic_cast<Derived*>(pBase);    // 类型安全。只有当pBase的确是指向Derived对象的时候,才能转,否则返回空指针.
Derived* d = (Derived*)(pBase);    // 不安全。如果pBase不是指向Derived对象的,则后面调用到Derived的函数的时候可能会崩溃。

如果不想把基类的析构函数修饰为virtual,则应该把基类的析构函数声明为protected的。这样子外部就没法使用基类指针的形式操作多态,但是仍然可以使用多态。
void testInfo(const Base& b){
    b.f();
}
void testBase(){
    Derived d;
    testInfo(d);
}

35、struct默认是public继承,class默认是private继承。

36、子类中如果有一个函数f需要重写父类的函数f,最好加override修饰,避免子类在书写该函数名的时候写错。

37、虚函数除了可以通过指针的方式调用,也可以通过引用的方式调用。

38、在父类的构造函数里面调用虚函数,调用的是父类自己定义的函数。
在父类的析构函数里面调用虚函数,调用的是父类自己定义的函数。
也就是说在构造函数和析构函数里面调用虚函数f,此时f不具备虚函数的特性。
因为万一在父类的构造函数里面能够调用到子类的虚函数,而子类的虚函数里面用到了某个成员变量,在C++中,是先调用基类的构造函数再调用子类的构造函数的,此时该成员变量其实还不存在,所以就有问题了。

39、建议使用以下的方式遍历容器。因为for-range效率应该会更高。
for(auto item : vec){
    ...
}
for(auto& item : vec){
    ...
}
for(const auto& item : vec){
    ...
}

40、建议用{}进行初始化,而不用().
class MyClass{
public:
    explicit MyClass(int nValue) : m_nValue(nValue){}
private:
    int m_nValue;
}
MyClass m1(1.1);    // 允许,相当于传入了1
MyClass m2{1.1);    // 不允许,只允许向上转型,例如传入true就可以,因为true是1,占1字节,1.1是double类型,占8字节.

要注意变量的初始化.
class MyClass{
public:
    MyClass(int nValue1, int nValue2)
    : m_nValue1(nValue1), m_nValue2{nValue2}{}
    MyClass() {}
private:
    int m_nValue1{};        // int的默认值就是0.
    int m_nValue2{100};
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值