四、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};
}