C++细节补充

版权声明: https://blog.csdn.net/wubaobao1993/article/details/79117150

写在前面

最近写了一个飞控的程序,主要用C++编程,里面涉及到一些矩阵的运算,因此就规规矩矩的写了一些关于向量和矩阵的类和运算,期间也走了一些弯路,具体的正文里面会说一下,但是今天在ubuntu下面进行C++的验证,居然让我十分诧异,有些特性真的已经颠覆了我的认知,当然,这些特性着实帮助C++变得更加方便了。(初次更新)

近来进行g2o源码的理解与demo的编写,那么在自己跟着教程写demo的时候,无意间发现了一个问题,该问题主要还是自己对dynamic_cast和static_cast的理解不清导致的,上网查了好久,才解决了这个问题,这里郑重记录下。(第一次补充)


缘起一段飞控程序

这段时间闲来无事,就想着做一段飞控程序,验证一下刚知道的算法, 其中涉及到好多矩阵和向量的运算,当然这些很可以用二维数组加一些for循环解决,但是为了更好的组织代码,就想着还是写一些类把这些东西都给封装起来,一来是好看,二来也挑战一下这些基础知识点的运用。

在写的过程中,确实发现有些基础的知识点真的很薄弱,这里也作为提醒写下来:

1. 运算符的重载。这个看似很简单常见的知识点,里面的坑还真的是不少的,比如方括号的重载(operator[]),本来以为这个很简单,但是实际写了才明白,哦,原来还有左值和右值的区分:x 左值也就是在等号的左边,例如matrix[1]=vectorX,右值也就是在也就是在等号的右边,例如vectotX v = matrix[1],具体声明如下:

这里只给出了声明,他们的定义是一样的,都是把要的值给return出来(这里一定要用引用哈)。

2. const的限制。这个我觉得也是比较坑的一点,但是这个坑不是很深,就是如果你在调用函数传参数的时候在类(class)前用了const修饰,那么在这个函数中使用这个类的方法的时候,就只能使用const修饰的函数,例如下面的例子。

此时程序里面没有提示错误,但是如果我们吧Matrix前用const修饰的话呢?

这时候程序提示说Called object is not a function,其实主要的问题就是因为程序在重载括号的时候,并没有限制说明自己不改变该类中成员的值,如果把程序改为下面的形式,则程序就很OK了!


上段程序里面重载()的时候,程序告诉了编译器我不会修改类中的值(得益于函数后面的const),因此在operator+的函数里面,编译器才会放心放你调用括号的重载。


那么经历的磕磕绊绊之后,终于把程序写出来了,也运行了起来,到此程序的缘分结束。


再次编写的发现

最近又得空,在CLion中重新写了该部分的程序,这次我只是抱着试试看的态度稍微改了一下程序,居然发现现在C++也变得如此方便,说一个最让我惊奇的,也就是重载加法运算符,在KEIL中(甚至说Visual Stdio里面),上面的代码都会引起一个很致命的bug,俗称野指针!这个野指针的产生主要是因为作用域的关系,正常情况下,C和C++的规定告诉我们说一个变量(不是new的)的生命周期是在大括号的支配下的,大括号结束,那么该变量就进入析构函数了,如果这时候析构函数中没有内存的释放还好,也刚好这时候你的程序不会发生问题,但是一般情况下,析构函数中都是会释放内存同时把指针归NULL的(不归NULL的也是野指针),这个时候如果采用了上图的编程方式,那么运行过程中就会出现错误,因为此时你的指针根本不知道指的什么地方,再次调用或者写入会导致严重的后果。但是,重点来了,上面的代码在CLion下居然可以完美的运行,而且我测试的结论是,上图中的retMat的生命周期根本就没有在大括号结束的时候结束,当然不是全部情况,下面就是两种情况。

第一种:定义的时候进行赋值


这时候可以看到程序是完美的运行,而且可以看到,按照正常的逻辑,在输出fenjiexian之前,终端应该会输出析构函数的提醒(因为operator+里面有retMat的结束),但是可以看到,程序并没有输出,而是在最后整个程序结束的时候,按照栈的形式把各个matrix进行了析构。

第二种:先定义再赋值

可以看到,程序按照我们的原先的想法,先结束了retMat的生命周期,之后再把整个类中的变量浅复制给要赋值的变量,而后就如程序输出一样,开始乱了!更不要说以后的运算了,只要程序不死就很清醒了。经过我的实际测试,在Visual Stdio里面,即使采用第一种方法的编程方法也不能达到第一种方法的效果(VS2013),所以在写飞控的时候,只能各种new,然后保留着这些内存,留意着释放。

除了上述的特性,我还发现在CLion下,重载的左值右值也显得不重要了,按正常的左值方法写就都可以用了。


dynamic_cast和static_cast的基本功能

那么他们的基本功能相比对于C++熟悉的就比较明白,主要是上行转换(upcast)和下行转换(downcast),其中还或许有单边转换(sidecast)和十字转换(crosscast),只不过后面的情况确实比较少,这里不做介绍了。

上行转换主要是子类指针指向父类的对象,这里一般都不会有很大的问题,毕竟子类在初始化的时候就包含了父类的初始化。

下行转换主要是父类指针指向子类的对象,这里注意在定义的时候千万不能直接进行定义,也就是如下的情况:

#include <stdio.h>
#include <iostream>

using namespace std;

class A {
public:
    // print
    virtual void names() const {
        cout<<"I'm class A"<<endl;
    }
};// end of class A

class B:public A{
public:
    // print
    virtual void names() const {
        cout<<"I'm class B"<<endl;
    }
};// end of class B

int main(){
    B* pb = new A;  // 这是不允许的
}// end of main

然后就是两个转换的显式优缺点:static_cast在转换的时候不进行可行性的检查(这个缺点主要在下行转换的时候表现出来,官方也称为不安全的,但是只要你确定这是一个安全的下行转换,那么就没有问题!),dynamic_cast在转换的时候进行可行性检查,并且能够比较智能的判断转换到中间的某个基类上(sidecast),但是dynamic_cast只能转换具有多态性的类的指针或者引用,也就是其中有虚函数的类,同时也要注意只能是指针或者引用哦!

使用dynamic_cast的时候需要注意的情况

1. 不能用dynamic_cast对普通的类进行转换,甚至继承关系也不可以!官方文档上指出dynamic_cast进行转换的必须是含有多态的类,也就是里面必须自己有或者继承自别处的虚函数!

2. 在下行转换的时候,有一种情况是不能使用dynamic_cast的,也就是整个继承关系中不能有虚函数只有声明而没有定义(特别是我们在使用一些库的时候,作者给出的基类很多都是抽象类)!即如下的情况:

#include <stdio.h>
#include <iostream>

using namespace std;

class A {
public:
    // names函数只有声明没有定义
    // 同样适用于virtual void names();但是这时候编译会提示A中的问题,此时这个类是不能用的,因为有函数没有定义
    virtual void names()  =0;
    // where有声明也有定义
    virtual void where() {
        cout<<"in class A"<<endl;
    }
};// end of class A

class B:public A{
public:
    virtual void names();
    // 这里覆写与否都是不影响的
    void where() {
        cout<<"in class B"<<endl;
    }
};// end of class B

int main(){
    A* pa = new B;
    B* pb = dynamic_cast<B*>(pa);
    if(pb)
        pb->where();  
    // B* pb = new A;  // 这是不允许的
}// end of main

此时编译的时候会提示如下信息(这个情况下是说明子类(B)中并没有把父类中的纯虚函数定义了)
对‘vtable for B’未定义的引用
对‘B::names()’未定义的引用
对‘typeinfo for B’未定义的引用

如果出现下面的信息(这个情况下是说明父类(A)中的虚函数没有定义,那么这个类是不能用的)

对‘vtable for A’未定义的引用
对‘A::names()’未定义的引用
对‘typeinfo for A’未定义的引用

这里也是因为不常见,网上直接搜索问题又搜索不到,这里记录下来,希望能帮助更多的小伙伴。

那么解决方案也比较简单,主要有两种(注意,不适用于父类中只有虚函数(不是纯虚函数)声明而没有定义的):

1. 在父类中下手,把所有的纯虚函数定为虚函数并定义;

2. 从子类中下手,把父类所有的纯虚函数都给覆写掉;

3. 抛弃dynamic_cast,使用static_cast,但是这时候要注意了,没有覆写的函数在程序中千万不要用了。



总结

上述里面,我一直说是在CLion下进行运行,其实更确切的讲,这些特性应该是C++11中的改变,只不过我大致看了一下C++11的一些改进,并没有看到与之相关的名词(确切的说是我也不知道这样的特性怎么形容),所以不敢断定的说就是C++11标准的优势,不过有这样的改变,对于我来讲是很高兴的,毕竟不用再想着生命周期了!不过今天看了一个知乎帖子,感觉也很有道理,不管三七二十一,全部用shared_ptr就好了,仔细想来这方法的确很实用:D


最后,以上均代表个人理解和观点,如有错误,还请大神们能够批评指正,谢谢!

猜你喜欢

转载自blog.csdn.net/wubaobao1993/article/details/79117150