重学C++系列(一):从C到C++

2,959 阅读5分钟

🔥 Hi,我是小余。 本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!

前言

作为一个Android开发者,可能你觉得我是不是跑错场了,Android开发又用不到C++的知识。。 额,如果你这么觉得,只能说明你还是一个Android基础开发者,C++在高级领域,如性能优化,NDK,音视频,framework,ART虚拟机等都使用的它,所以学习C++对我们Android开发其实非常必要。

本篇是重学C++系列的第一篇,讲述了一些C/C++开发中可能出现的错误并分析如何规避,希望文章对你有启发

目录

C_C++langbug.png

1.char类型以及char* 类型的变量初始化问题

案例:

​
​
void charBug() 
{
    char c1 = 'yes';//截断,取最后一个字符:'s'
    char c2 = "yes";//报错:const char*类型的值不能用于初始化char类型的实体
    char c3 = &c1;//报错: char*类型的值不能用于初始化char类型的实体const char* cs1 = '/'; //报错:char 类型的值不能用于初始化const char*类型的实体
    const char* cs2 = "/";//正确取值为:'/''\0'
    const char* cs3 = &c1;//正确取到c1的地址值
​
}

上面代码表明了:

  • 1.字符串作为等号右边数值,则给左边变量赋值的是该字符串常量的地址。
  • 2.char*类型的值(如一个字符串或者字符的地址),不能赋值给一个char类型的变量。
  • 3.char类型的值作为右值不能直接赋值给一个char*类型的变量。

那在C++中怎么去规避这些操作呢?使用string

string s1(1, 'yes');//s
string s2(3, 'yes');//sssstring s3("yes");//yes
string s4("/");// /
string s5(1,'/');// /

C++中使用string规避了这些因为char*和char赋值一起的编译器错误。

2.C语言数组作为参数退化问题

案例:

double average(int arr[]) {
    double result = 0.0;
    int len = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < len; i++) {
        result += arr[i];
    }
    return result/len;
}
int main()
{
   cout << "Hello World!\n";
   //charBug();
   int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
   cout<<average(arr)<<endl;
​
}
打印结果为:1 

说明在函数内部的是arr其实是一个指针了,更好命名应为p_arr,一个指针的sizeof为4个字节,而arr[0]也是指针所以len=1。C语言设计者考虑的是不能将一个大的容器传递到一个函数内部,而只传递容器的地址,用于节省空间。

下面来看C++是如何优化这个问题的。 使用STL中的容器来计算:

double average2(vector<int>& vec) {
    double result = 0.0;
​
    vector<int>::iterator it = vec.begin();
    for (; it != vec.end(); ++it) {
        result += *it;
    }
    return result / vec.size();
​
}
vector<int> vec{ 1,2,3,4,5,6,7,8,9,10 };
cout << average2(vec) << endl;
​
打印结果:5.5

输出了正确的平均结果

3.C语言移位问题

案例1:

void bitMove() {
    char c1 = 0x63;//0110 0011
    c1 = c1 << 4;//右移4位
    printf("0x%02x\n",c1); //推算:0011 0000 ->0x30c1 = 0x63;
    c1 = c1 >> 4;//左移4位
    printf("0x%02x\n", c1);//推算:0000 0110 ->0x06
    
    char c2 = 0x93;//1001 0011
    c2 = c2 << 4;//右移4位
    printf("0x%02x\n", c2); //推算:0011 0000 ->0x30
    
    c2 = 0x93;
    c2 = c2 >> 4;//左移4位
    printf("0x%02x\n", c2);//推算:0000 0110 ->0x09?
​
}
​
结果:
0x30 0x06 0x30 0xfffffff9

看到最后一个值为0xfffffff9,推算因为0x06. 这是由于算术位移和逻辑位移的区别导致的。

逻辑位移

对于无符号数,左移时对低位补0,右移对高位也是补0.

算术位移

对于无符号数的正数因为三码一致,则和逻辑位移的补齐方式一致,对于有符号数的负数,则左移时对低位补0,右移时则对高位补1.所以就发生前面的0x93在左移的过程中使用的是高位补1,得到的就是0xfffffff9形式,因为负数使用的是算术位移。

那C语言中如何规避这种问题的发生了?考虑将有符号数转为无符号数即可:char -> unsigned char

这里还有个点为什么是0xfffffff9而不是0xf9,题中明明是使用的char类型啊,为啥会有4个字节啊, 原因就是%x要求的是无符号整形变量,如果传入的是char类型,则会有一个整数提升的过程,由于0xf9是一个负数char,提升的时候会对高位使用1填充,所以得到的就是0xfffffff9,而不是0xf9。

修复方式也是将有符号数转为、无符号数:char -> unsigned char,这样高位会使用0来填充。

案例2:

void bitMove1() {
    unsigned char x = 0xFF;
    const unsigned char BACK_UP = (1 << 7);
    const unsigned char ADMIN = (1 << 8);
    //printf("0x%x\n", BACK_UP);
    //printf("0x%x\n", ADMIN);if (x & BACK_UP) {
        cout << "BACK_UP" << endl;
    }
    if (x & ADMIN) {
        cout << "ADMIN" << endl;
    }
​
}  

结果只打印除了BACK_UP,这是为啥呢? 我们把两个printf打开看看: 结果:一个0x80 一个0x00

原因位移数不能大于位数,如果移动超过位数就是全部移出,导致归0.

以上两个案例都是因无符号数位移引起的异常情况。

那么在C++中如何规避的呢?使用bitset

void bitMove2() {
    bitset<10> x = 0xFF;
    const bitset<10> BACK_UP = (1 << 7);
    const bitset<10> ADMIN = (1 << 8);
    printf("BACK_UP:0x%x\n", BACK_UP);
    cout<<"BACK_UP:binary:" << BACK_UP << endl;
    printf("ADMIN:0x%x\n", ADMIN);
    cout << "ADMIN:binary:" << ADMIN << endl;
​
    if ((x & BACK_UP) == BACK_UP) {
        cout << "BACK_UP" << endl;
    }
    if ((x & ADMIN) == ADMIN) {
        cout << "ADMIN" << endl;
    }
​
}
结果:
BACK_UP:0x80
BACK_UP:binary:0010000000
ADMIN:0x100
ADMIN:binary:0100000000
BACK_UP

可以看到bitset指定了一个位长度10的无符号数,所以在移动8位后,得到的是0x100,而非0.

4.C语言中的强制转换问题

案例1:

void cast1() {
    int arr[] = { 1,2,3,4 };
    cout <<"arr size:" << sizeof(arr) / sizeof(arr[0]) << endl;
    int thresholp = -1;
    if (sizeof(arr) / sizeof(arr[0]) > thresholp) {
        cout << "arr len is big than thresholp" << endl;
    }
    else
    {
        cout << "thresholp is big than arr len" << endl;
    }
    cout << (unsigned int)(-1) << endl;
}
结果:
arr size:4
thresholp is big than arr len
4294967295

结果-1居然比4大,对一些不了解底层细节的同学会比较懵逼。

重点在sizeof(arr) / sizeof(arr[0]) > thresholp这个判断,涉及了一个强转转换的问题

  • 1.sizeof(arr) / sizeof(arr[0])返回的是一个unsigned int类型的值,thresholp是一个int类型的值。

在计算机的比较中,如果无符号类型和有符号类型值进行比较,则会将有符号类型的一边转换为无符号类型,然后再进行比较,这是一个隐式 的强转操作。

int类型的-1转换为无符号后的值为:4294967295,所以得到的结果是thresholp is big than arr len

  • 2.在这个案例中,只需要使用一个int类型的值去存储sizeof(arr) / sizeof(arr[0])即可

    void cast1() {
        int arr[] = { 1,2,3,4 };
        cout <<"arr size:" << sizeof(arr) / sizeof(arr[0]) << endl;
        int thresholp = -1;
        int len = sizeof(arr) / sizeof(arr[0]);
        if (len > thresholp) {
            cout << "arr len is big than thresholp" << endl;
        }
        else
        {
            cout << "thresholp is big than arr len" << endl;
        }
    }
    

    两个都是有符号类型,不会进行强转,所以可以得到正确的结果。

案例2:

void cast2() {
    double result = 0.0;
    int arr[] = { 10,20,30,40 };
    unsigned int len  = sizeof(arr) / sizeof(arr[0]);
    for (unsigned int i = 0; i < len; i++) {
        result += (1 / arr[i]);
    }
    cout << result << endl;
    return;
}
结果为0,

其实问题在于“1 / arr[i]”,分子和分母都为int类型的情况下,则得到的也会是一个int型的结果,也就说会丢失进度。 对于分子为1的情况,则得到的结果都会为0. 如何改正呢? 只需要将(1 / arr[i])改为(1.0 / arr[i]),由于1.0 / arr[i]不会丢失精度,所以最后得到的是一个正确的浮点数。

通过上面的两个案例可以看出,C语言中的一些隐式转换会变为一些隐藏的bug和陷阱,且不容器排查

C++中也哪些解决方案? C++中可以使用显示的转换方式:static_cast,const_cast,dynamic_cast,reinterpret_cast

  • static_cast;等价于隐式转换的一种运算符,用来表示显示的转换。
  • const_cast:用来去除复合类型中的const和volatile属性
  • dynamic_cast:父子类指针之间转换
  • reinterpret_cast:指针类型之间转换,有一些风险

对案例2:

void cast2() {
    double result = 0.0;
    int arr[] = { 10,20,30,40 };
    unsigned int len  = sizeof(arr) / sizeof(arr[0]);
    for (unsigned int i = 0; i < len; i++) {
        result += (static_cast<double>(1))/ arr[i];
    }
    cout << result << endl;
    return;
}
结果:0.208333

正确结果

5.C语言整数溢出问题

案例1:

void intOverflow() {
    int i = 0x7ffffff0;//2147483632
    for (; i > 0; i++) {
        cout << "adding" << endl;
    }
    cout << "end ??" << endl;
}

第一眼觉得,i用于大于0,for循环不会结束。 实际结果:

adding
adding
adding
adding
adding
adding
adding
adding
adding
adding
adding
adding
adding
adding
adding
adding
end ??-2147483648

在执行了16次adding还是退出了,这是因为i是int类型的变量,有符号类型的整数:范围会在-2147483648~2147483647之间,数值添加到2147483647之间后,再加1会变为-2147483648 就好像时钟走了一圈后,又从0点开始一样

案例2:

void intOverflow2() {
    int a = 200, b = 300, c = 400, d = 500;
    cout << a * b * c * d << endl;
}
结果:-884901888

这里也是发生了整数溢出导致结果变为负数。

C++中如何解决这种大数溢出的问题?使用扩展库,如boost库www.boost.org

#include <boost/multiprecision/cpp_int.hpp>
using namespace boost::multiprecision;
​
void intOverflow2() {
    cpp_int a = 200, b = 300, c = 400, d = 500;
    cout << a * b * c * d << endl;
}
结果:12000000000

6.C语言字符串缺陷。

案例1:

void strTest() {
    char str1[] = "abcdef";
    cout << "str1_strlen:" << strlen(str1) << endl;
    cout <<"str1_sizeof:" << sizeof(str1) / sizeof(str1[0]) << endl;
​
    char str2[] = "abc\0def";
    cout << "str2_strlen:" << strlen(str2) << endl;
    cout << "str2_sizeof:" << sizeof(str2) / sizeof(str2[0]) << endl;
​
}
结果:
str1_strlen:6
str1_sizeof:7
str2_strlen:3
str2_sizeof:8

从上面的结果可以看出C语言中的字符串其实是以\0结束的,这就会限制很多应用场景,且运行效率低。

C++中的解决方案 使用C++中的string类或者一些开源库解决方案如redis库的实现redis.com github.com/redis

Redis 是一种开源(BSD 许可)内存数据结构存储,用作数据库、缓存、消息代理和流引擎。Redis 提供数据结构, 例如 字符串、散列、列表、集合、带范围查询的排序集合、位图、hyperloglogs、地理空间索引和流。

关于更多Redis的信息,可以自行查阅Redis官网。

小结

  • 1.C语言是一种高级语言的的低级语言,小巧,高效,接近底层,但是细节和陷阱较多
  • 2.C++完全继承了C语言的特性,但是提出了一系列更现代化和工程化的特性,包容性强,但是语言自身比较复杂。
  • 3.本章针对的字符,字符串,指针,数组,整数等表示,类型转化和移位等操作的说明,讲解了C的设计和C++中响应的解决方案,帮助大家搭好C/C++基础。我是小余,我们下期见。