行业知识
C++两万字长文总结
C++磅薄深邃,浩瀚如烟,让人又爱又恨,爱的是一切尽在掌控之中,恨的是掌控一切太难,大佬们睿智的头脑,演化出无数经典,被广大的信众奉为圭臬,后继者孜孜不倦,这门古老的语言也不断进化,泛发出新的生机。学习C++三年有余,一直想对其做一个整体的总结的,可能是管中窥豹,只见树木,不见森林,但确实是这几年爱之深,恨之切,不断积累钻研,反复被摩擦而又有所顿悟,缓慢前行而理解渐深的一个阶段性的总结,期间也手撸过STL,LevelDb这些经典的项目,也参与过大型C++项目的开发,也囫囵吞枣的看过不少书,回想起来还是学了不少东西。一直觉得, C++是一门充满魔鬼与细节的语言,不同阶段水平对其都有不同的感悟和理解,如果假装大佬模样,简单的甩出几句: 面向对象是灾难,虚函数一无是处,指针是大坑,新特性只是语法糖,这样看起来似乎不是新手了,但是一定是没有认真思考过这门语言。本文尽量从宏观的角度去感悟C++,尽量不做细节展开,一些主要的知识点也以思维导图的方式做简单回顾总结,具体细节,希望在后面更多文章中再做探讨。
一、从计算机的视角来看
冯洛伊曼体系
冯洛伊曼体系将计算机结构抽象为三大部分,输入输出设备是计算机和外界交互的入口,存储器是数据和指令存储的容器,中央处理器是计算机的大脑。CPU是计算机最核心的组建,事实上CPU是一个笨笨的大脑,它只会重复的干着读取指令,执行指令这样一件简单的事情,但是不要怀疑,它干的真的很快。
冯洛伊曼体系为计算机大厦奠定了坚实的基础,它以一种及其简洁的方式界定了计算机的能力,即解决一切可以用计算解决的问题。这一定义和数学上的函数y= F(x) 非常相似,你给它一个输入,经过一系列运算,就能给你一个输出。这是一种简介而又充满力量的抽象,纳须弥于芥子,以此为基,计算机的大厦拔地而起,岿然屹立。
硬件的工作方式是简单,暴力,大力出奇迹,他们只孜孜不倦的处理着二进制数据,维护着计算机这个复杂的状态机。但是这种方式显然不适合人类,人类面临的是更错综复杂而又具体的问题,人类并不善于处理二进制问题,于是更上层的软件出现了。软件是用魔法打败魔法,用复杂的二进制去解决更复杂的二进制问题,从机器码到汇编到高级语言,到各种基础类库和框架体系,软件也逐渐从根生长出茂密的森林。
软件的基础是编程语言,计算机发展至今,已经诞生了不知道多少们编程语言,但能够经久不衰,日益壮大,拥趸众多的语言其实不多。而C++,便是这万千种编程语言中最为璀璨的一颗之一。她既保留了底层硬件的直接访问途径,又具有完整的高级抽象,兼容并包,内修心法,外练招式,是程序员不可或缺的一环。
从硬件到软件到上层应用,每一层都将自己的能力封装起来,为上一层提供丰富的服务,这种服务就是能力边界的抽象,上层不用直接和底层的繁琐细节打交道,软件不需要过多的了解硬件,框架不需要过多的了解语言层面的实现, 在这简单的几层抽象体系体系下,可以总结一下计算机的科学的本质:
怎么在硬件层次解决问题?
- 硬件的性能:浮点运算能力,多处理器,并发,GPU等
- 硬件的协同: 硬件资源调度配合 , 系统中断 ,IO等
- 硬件的稳定性安全性:保证逻辑执行的准确性,稳定性等
怎么在软件层次解决问题?
- 操作系统,对资源的调度和利用
- 对硬件抽象,提供面向操作系统的统一接口
- 对软件抽象,提供面向编程的统一接口
自最底层到最顶层,随处可见c++身影,其丛深的深度足够深,但在不同层次所关注的重点不一样,正是C++的纵深性,造成其复杂性,让人常有不识庐山真面目,只缘身在此山中之感。古语云,学诗,功夫在诗外,对于C++,亦是如此:学C++功夫在C++之外。
二、从编程语言来看
编程语言
编程语言是一套逻辑符号语言和标准规范的合集,通过一套语法,语义,关键字,逻辑符号定义了描绘程序世界的语言。这套语言,只有人类认识,机器是不认识的,因而编程语言实现的关键是编译器,由编译器将代码翻译成机器指令,这样人类和机器之间的沟壑被打通。
第一门面向程序员的高级语言是汇编语言,虽然我没学过汇编语言,但是偶尔碰到疑难杂症,调试的时候跳转到汇编代码,由于汇编代码几乎是指令的简单映射,因而也能看懂个一二, 还是挺能装一装有点技术含量的。那么问题来了,汇编语言是第一门编程语言,那么是什么语言编写了汇编编译器? 总不能是他自己吧?我没有去挖掘过这个问题,我想应该是上古时期优秀的计算机科学家,用机器码编写了汇编编译器,想想也是很痛苦。
不过,编程语言是可以自举的,是可以不断进化的,汇编语言现在不是有了吗,好,那我用汇编语言编写一个更牛逼汇编编译器可以吧,我用更牛逼的汇编编译器可以编译更复杂的汇编语言了,还不过瘾,我又用汇编写了一个C语言编译器,然后又用c语言写了一个更更厉害的汇编编译器,怎么样,这波套娃是不是很厉害。从机器指令到底层汇编,C语言,Fortran语言,再到高层抽象的C++,Java,Python,生产力逐渐释放~
C++语言基本要素
编程语言至少包含如下三要素, 基本上有这三个要素就能定义一切能够用计算描述的世界了
- 循环和条件分支:让你具有做选择和做重复工作的能力
- 变量和基本运算:计算世界的基本表达
- 函数:定义复杂的计算过程
循环和条件分支
C++中的循环和条件分支是控制程序执行流程的基本结构,以下是 C++ 中常用的循环和条件分支结构
条件语句 if、else if、else
if (condition1) { } else if (condition2) {} else {}switch 语句
switch (expression) { case value1: break; case value2: break; default:}循环结构 for 循环
for (int i = 0; i
循环结构 - while 循环
int i = 0;while (i
循环结构 - do-while 循环
int i = 0;do {// 循环体,至少执行一次,然后检查条件 ++i;} while (i循环和选择是复杂程序流程的基本,可以说这些内容占了程序的80%,但是可能和想的不一样,优秀的代码要尽量避免复杂的循环和选择,一方面,代码应该尽量追求简介明了,复杂的代码不易于阅读和维护。另一方面,分支选择是会影响性能的。用复杂的if else逻辑来翻译业务流程很简单,但是经过巧妙设计,干掉复杂的分支选择则需要一定的功夫。在性能方面,分支选择涉及到指令跳转,cpu指令流水线的工作方式,会进行分支预测,从而提高指令处理效率,如果分支选择过于复杂,自然会影响到指令流水线工厂的效率,有一个小技巧就是将大概率执行的if分支写在前面,有些编译器还提供if-likely,unlikely这样的写法,C++20也加入了likely,unlikely属性关键字,用以优化效率。
变量和运算
编程语言可以分为静态类型和动态类型两类,在静态类型语言中,变量的类型在编译时就已经确定,程序员在声明变量时必须明确指定变量的类型。编译器会进行类型检查,确保变量在使用时符合其声明的类型。如果存在类型不匹配的错误,编译时就会产生错误,C,C++,Java这些都是强静态类型。在动态类型语言中,如python,jave script,变量的类型是在运行时确定的。程序员无需显式声明变量的类型,变量的类型可以随时改变。类型检查通常是在运行时进行的,动态类型就少了很多类型相关的负担。
C++数据类型
C++有多种数据类型,对于数据类型,要理解两点,一是数据的表示方式,如有符号还是无符号,浮点数编码等,二是数据的内存布局,不同类型的数据在内存中的长度不同,表示的范围也不同,需要注意大数溢出的问题,复合数据类型,如结构体还有内存对齐的问题。
C++还具有丰富的类型修饰符和类型别名的能力,允许程序员在需要时创建更复杂的数据结构,如typedef和using,合理的用用typedef,可以减轻很多类型的负担。
对于不同的机器,数据的表示还有大小端之分,小端即数据的低位存储在低地址,高位存储在高地址,如下为检测大小端的简单代码。
// true: 小端// false: 大端bool IsLittleEndian(){ uint16_t num = 1; return (*reinterpret_cast(&num) == 1);}左值和右值
C++中,左值 和右值是两个很重要的概念,注意值类别和类型是两个完全不同的概念,c++中左值右值类别的划分还是挺复杂的,可以参考下图
我们先理解一下这些名词的字面含义:左值lvalue是通常可以放在等号左边的表达式,右值rvalue 是通常只能放在等号右边的表达式,glvalue是广义左值,xvalue 将亡值(expiring value),prvalue 是纯右值(pure rvalue),左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
- 变量、函数或数据成员的名字
- 返回左值引用的表达式,如 ++x、x = 1、cout 聚合>关联>依赖。
✍️以桌子为例来说明一下各种关系:桌子为泛化,餐桌为实现,桌子和木头是依赖关系, 桌子和桌子上的果盘 是关联关系, 桌子和桌布聚合关系, 桌子和桌腿是组合关系。
一般通过UML(Unified modeling language ,统一建模语言),以符号图标的形式只管的表示类之前复杂的关系。
C++其他主要特性
指针
指针也是一种变量类型,它存储另一个变量的地址。我们通过type*来声明指针类型,需要在使用前进行初始化,使用解引用操作符*访问指针指向的内存中的值。对于类对象,可以通过->访问成员,->本质上也是一种操作符重载,表示指针所指向的对象。在C++中指针无处不在,正是指针赋予了C++与内存打交道的能力,当然能力越大,责任越大,这种能力的背后是C++程序员更高心智负担。
- 指针和动态内存分配
- 指针运算
- 指针和数组
- 指针和函数
- 智能指针
指针的精妙之处在于见微知著,以小博大
- 指针的大小书固定的,但是却能引用任意大小的数据,这在函数参数传递,和类的前置申明方面有着绝佳的应用
- 指针,指向一个地址,更是指向一块内存的起始地址,通过地址和长度,可以方便的索引一组在内存中连续的对象,最典型的例子就是数组
指针最核心的思想就是映射,一个指针可以映射一块连续内存,只要知道这块内存的地址,元素的类型和元素的个数,就能通过简单的指针偏移访问内存块中所有的对象,有以小见大,四两拨千斤之效。
指针本身也是一个变量,这个变量自身可以取地址,这个变量的值是一个地址, 指针类型决定了怎么去解析指针变量存储的地址里面的数据。由于指针本身是变量,因而可以套娃定义多级指针,但是这会让大大增加指针的复杂性。这里我有一个经验就是,遇到一个复杂的指针场景,不妨站在编译器的角度思考一下,从你给的指针信息出发,我该怎么去解释某一块内存,如果能够正确的解释,就是正确的使用指针了。
指针和引用
指针和引用在一定程度上是等价的,引用变量在功能上等于一个指针常量,即一旦指向某一个单元就不能在指向别处。那么既然引用和指针等价,为什么还需要引用?
经非官方考古研究,最初引入引用的原因是为了解决指针在某些运算符重载的情况下表现得非常拧巴,假设有如下示例中的一个数组类,通常需要重载[]操作符,用于通过下标访问数组元素,如果operator[]的返回值是指针,那么通过[]运算符对数组赋值只能写成*(a[0]) = 10,显然啰哩吧嗦。但是如果返回一个引用,就可以直接写成a[0] = 10,简洁明了。
class IntArray{public://int* operator[](size_t i); //返回指针int& operator[](size_t i); //返回应用};IntArray arr;//*(a[0]) = 10;arr[0] = 10;也许类似于上面的例子,是“引用”这个语言特性的引入契机,但引用相对于指针还有更多的优势:
- 从语法上,指针取值和访问成员的写法是:*ptr 和 ptr->foo,而引用的写法是 ref 和 ref.foo
- 引用可以 直接把函数的返回值当做左值,直接对函数的返回值进行修改,例如cout
上一篇:宝塔面板手机怎么登陆?