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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: C++在高级领域,如性能优化,NDK,音视频,framework,ART虚拟机等都使用的它,所以学习C++对我们Android开发其实非常必要。
🔥 Hi,我是小余。 本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!

前言

作为一个Android开发者,可能你觉得我是不是跑错场了,Android开发又用不到C++的知识。。

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

本篇是重学C++系列的第一篇,希望文章对你有启发

目录

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.const char类型或者char类型的值(如一个字符串或者字符的地址),不能赋值给一个char类型的变量。
  • 3.char类型的值作为左值不能直接赋值给一个char或者const char类型的变量。

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

string s1(1, 'yes');//s
string s2(3, 'yes');//sss

string 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 ->0x30

    c1 = 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库https://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库的实现https://redis.com https://github.com/redis

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

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

小结

  • 1.C语言是一种高级语言的的低级语言,小巧,高效,接近底层,但是细节和陷阱较多
  • 2.C++完全继承了C语言的特性,但是提出了一系列更现代化和工程化的特性,包容性强,但是语言自身比较复杂。
  • 3.本章针对的字符,字符串,指针,数组,整数等表示,类型转化和移位等操作的说明,讲解了C的设计和C++中响应的解决方案,帮助大家搭好C/C++基础。我是小余,我们下期见。
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
4月前
|
设计模式 API 图形学
Unity精华☀️ 「设计模式」的终极详解!
Unity精华☀️ 「设计模式」的终极详解!
|
4月前
|
设计模式 安全 图形学
Unity精华☀️ 面试官眼中的「设计模式」
Unity精华☀️ 面试官眼中的「设计模式」
|
4月前
|
图形学
Unity精华☀️点乘、叉乘终极教程:用《小小梦魇》讲解这个面试题~
Unity精华☀️点乘、叉乘终极教程:用《小小梦魇》讲解这个面试题~
|
7月前
|
移动开发 前端开发 JavaScript
Web前端开发之面试题全解析 一(3),前端面试题背不下来怎么办
Web前端开发之面试题全解析 一(3),前端面试题背不下来怎么办
|
设计模式 JSON 监控
趣谈装饰器模式,让你一辈子不会忘
来看这样一个场景,上班族大多有睡懒觉的习惯,每天早上上班都时间很紧张,于是很多人为了多睡一会儿,就用更方便的方式解决早餐问题,有些人早餐可能会吃煎饼。煎饼中可以加鸡蛋,也可以加香肠,但是不管怎么加码,都还是一个煎饼。再比如,给蛋糕加上一些水果,给房子装修,都是装饰器模式。
136 0
|
7月前
|
前端开发 JavaScript
前端知识(十五)——es6 相关面试总结
前端知识(十五)——es6 相关面试总结
61 0
|
设计模式 前端开发 JavaScript
图解23种设计模式(TypeScript版)——前端切图崽必修内功心法
图解23种设计模式(TypeScript版)——前端切图崽必修内功心法
图解23种设计模式(TypeScript版)——前端切图崽必修内功心法
|
设计模式 Java API
听说有人用一个坦克大战项目把23种设计模式讲完了?(附源码)
长期以来给大家分享的都是技术和文档的一些内容,大家应该已经看腻了。今天给大家分享一波java的坦克大战项目和23种设计模式视频吧,让大家来实践一下,希望大家能够喜欢!
|
前端开发 自动驾驶 算法
这个知识点99%的前端都没有听过,不信你进来看?
这个知识点99%的前端都没有听过,不信你进来看?
105 0
|
前端开发 JavaScript API
重学前端 26 # CSSOM
重学前端 26 # CSSOM
167 0
重学前端 26 # CSSOM
下一篇
DataWorks