微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

C++的缺陷和思考七

本文继续来介绍C++的缺陷和笔者的一些思考。先序文章请看
C++的缺陷和思考(六)
C++的缺陷和思考(五)
C++的缺陷和思考(四)
C++的缺陷和思考(三)
C++的缺陷和思考(二)
C++的缺陷和思考(一)

语言、STL、编译器、编程规范

笔者认为,C++跟一些新兴语言最大的不同就在于将「语言」、「标准库」、「编译器」这三个概念划分为了三个领域。在前面章节提到的一系列所谓“缺陷”其实都跟这种领域划分有非常大的关系。

举例来说,处理字符串相关问题,但是使用std::string就已经可以避免踩非常多的坑了。它不会出现0值截断问题;不会出现拷贝时缓冲区溢出问题;配合流使用时不会出现%s不安全的问题;传参不必在意数组退化指针问题;不必担心复制时的浅复制问题……

但问题就在于,std::string属于STL的领域,它的出现并没有改变C++本身,最直观地来讲,字符串常量"abc"并没有映射到std::string类型,它仍然会按照C风格字符串来处理。它就有可能导致重载、导致模板参数识别不符合预期。除非我们将其转换为std::string

所以说,虽然std::string解决了绝大对数原始字符串可能出现的问题,但它是在STL的维度来解决的,并不是在C++语言的维度来解决的。接下来我会详细介绍这三种领域之间的关系,以及我个人的一些思考。

C++与STL的关系

虽说STL是“C++的标准库”,但C++和STL的关系是不如C和C标准库的关系的。主要的区别是:

C标准库的实现基本是用汇编写的,而STL是完全用C++写的。

听上去可能不足为奇,但仔细想想这种差异可谓天壤之别。C库用汇编实现,也就意味着OS要原生支持这种功能,不同架构下的汇编是不同的。比如说Intel芯片的Mac电脑,它自带的C库就要用x86汇编(准确来说是AMD64汇编)来实现,而M系列芯片的Mac电脑,它自带的C库就要用ARM汇编来实现。

用C语言开发OS的时候确实没法使用标准库,但同时,我们没法做到仅用C语言来开发OS,它不可避免地要和汇编进行联动。而在用C开发应用程序的时候,OS就会提供C标准库的对应实现,也就是说在编译C程序的时候,标准库的内容是不用编译的,一遍都是作为静态链接库直接参与链接。(还有一些可能是动态链接库,运行是调用,但这个就跟OS和架构有关了。)

但STL不同,STL我们可以轻松看到其源码,它就是用C++来实现的。在C++工程编译时,STL要全程参与编译。

再说得笼统一点:你没法用C语言实现C标准库,但完全可以用C++实现STL,与此同时,如果你要用C++来实现STL的时候,你也不能没有C标准库。所以STL单纯是一些功能、工具的封装,它并没有对语言本身进行任何扩展和改变。

在C++诞生的时候,并没有所谓标准库,那个时候的C++其实就是给C做了一些扩充,所以用的仍然是C的标准库。只不过后来有位苏联的大神利用C++写了一个工具库,所以准确地来说,STL原本就只是个第三方库,是跟C++语言本身没什么关系的,只不过后来语言标准协会把它纳入了C++标准的一部分,让它成为了标准库。

所以“容器”“迭代器”“内存分配器”等等这些概念都是STL领域的,并不跟C++语言强绑定。另一方面,到后来STL其实是一套规定的标准,比如说规定要实现哪些容器,这些容器里应当有哪些功能。但其实实现方法是没有规定的,也就是说不同的人可以有不同的实现方法,它们的性能问题、设计的侧重点可能也不一样。历史上真实出现过某个版本的STL实现,由于设计缺陷导致求size时时间复杂度是O(n)的情况。

之前有读者读过我的文章后有发出质疑,类似于「如果你这么担心内存泄漏问题的话,为什么不用智能指针?」或者「如果你觉得C风格字符串存在各种问问题为什么不用stringstring_view」这样的问题。那么这里的问题点就在于,无论是string也好,还是智能指针也好,这些都是STL领域的,并不是C++语言本身领域的。所以一来,我希望读者能够明白STL提供这些工具是为了解决哪些问题,为什么我们使用了STL的这个工具就不会踩坑,工具内部是怎么避坑的;二来,给一些C++的新人解开疑惑,他们可能会奇怪,明明直接打一个双引号就是字符串了,为什么还要用string或者string_view。明明打一颗星就是指针了,为什么还要用shared_ptrweak_ptr等等;三来,也是倡导大家尽可能使用STL提供的工具,而不是自行使用底层语法

我曾经有过一个疑问,就是说为什么C++不能在语言层面上支持STL。举例来说,"abc"为什么不干脆直接映射成std::string类型?而是非要通过隐式构造的方式。为什么不能直接引入类似于{k1:v1, k2:v2}的语法来映射std::map?而是非要通过嵌套构造的方式。后来我大概猜到了原因,其实就是为了兼容性。设想,如果突然引入一种类型的强绑定,那么现有代码的行为会发生很大的变化,大量的团队将不敢升级到这个新标准。另一方面,有些特殊的项目其实是对STL不信任的,比如说内核开发,嵌入式开发。他们对性能要求很高,所以类似于内存的分配、释放等等这些操作,都必须非常小心,都必须完全在自己的掌控之中。如果使用STL则不能保证内部操作完全符合预期,但与此同时又不想使用纯C,因为还希望能使用一些C++的特性(比如说引用、类封装、函数重载等等)。那他们的选择就是使用C++但禁用STL。一旦C++语法和STL强绑定的话,也会劝退这些团队。

所以,这就是一个取舍问题,C语言保留着最基础、最底层的功能。而需要快速迭代、屏蔽底层细节又不是特别在乎性能的项目则可以选择更高级的语言。而C++的定位就是在他们之间搭一座桥,如果你是写底层而会的C++,你也可以转型上层软件而不用学习新的语言,反之亦然。总之,C++定位就是全能,可上可下。但正犹如细胞分化一样,越全能的细胞就越不专一,当你让它去做一种比较专一的事情的时候,它可能就显得臃肿了。但其实,C++提供庞大而复杂的功能后,我们完全可以根据情况使用它的一个子集,完成自己的需求就好,而不用过分纠结C++本身的复杂性。

编译器优化

编译器的优化又属于另一个维度的事情了。所谓编译器的优化就是指,从代码字面上脱离出来,理解其含义,然后优化成更高性能的方式。

举个前面章节提到过的例子来说:

struct Test {
  int a, b;
};
Test f() {
  Test t {1, 2};
  return t;
};

void Demo() {
  Test t = f();
}

如果按照语言本意来说,这里就是会发生2次复制,f内部的局部变量复制给临时区域(拷贝构造),再临时区域复制给Demo中的变量(移动构造)。

但是编译器就可以对这种情况进行优化,它会直接拿着Demo中的t进到f中构造,也就是说,编译器“理解”了这段代码的含义,然后改写成了更高性能的方式:

struct Test {
  int a, b;
};
void f(Test *t) {
  new(t) Test {1, 2};
}

void Demo() {
  Test t = *(Test *)operator new(sizeof(Test));
  f(&t);
}

这也就是编译器的RVO(Return Value Optimization,返回值优化)。

当然,编译器不止这一种优化,还会有很多优化,对于gcc来说,有3种级别的优化编译选项,-O1-O2-O3。会对很多情况进行优化。这么做的意义也很显而易见,就是说让程序员可以尽可能屏蔽这些底层语法对程序行为(或者说性能)的影响,而可以更多聚焦在逻辑含义上。

但笔者希望传达的意思是,“语言”、“库”、“编译器”是不同维度的事情。针对同一个语言“缺陷”,库可能有库的解决方法,编译器有编译器的优化方案,但是不同的库实现可能倾向性不同,不同的编译器优化程度也不同。

编程规范

编程规范又是一个完全不一样的维度,它一般是在比较固定的场景下,为了防止出现错误,而进行的一种上层约束。与编程规范一同使用的还有一些代码扫描工具。

笔者认为,编程规范主要是要考虑项目或者团队的实际情况,从而制定的一种标准。除了一些格式、代码风格上的统一以外,其他任意一条规范都一定有其担忧道理。可能是团队以前在这个点上踩过坑,也可能是以团队的平均水平来说很容易踩这个坑,而同时又有其他避坑的方式,因此干脆规定不许怎么怎么样,必须怎么怎么样。对于个人来说,有时可能确实难以理解和接受,甚至觉得有些束手束脚。但毕竟人心都向自由,但对于团队来说,要找到的是让团队更加高效、不易出错的方式。

有人说小白都不会质疑规则,大佬才会看得出规则中有哪些不合理。从某种角度来说,笔者认为这种说法是对的,但还应该补充一句“真正的大佬则是能看得出这里为什么不合理”。如果你能看得出制定这条规则的人在担心些什么,为什么要做这样约束的时候,那我相信你的视野会更宽,心也会更宽。

因此,如果你认为你所在团队的编程规范中槽点很多,那笔者认为,最好的方式就是提升团队整体的水平,就拿C++来说,如果多数人都能意识到这个位置有坑,应当注意些什么,并且都可以很好的处理这部分问题的话,那我相信,规范的制定者并不会再去出于担心,而强行对大家进行束缚了。

思考

尽管C++语言由于历史原因留下不少缺陷,但随着版本迭代,STL和编译器都在做着非常多的优化,所以其实对于程序员来说,日常开发真的不用太在意太纠结这些细枝末节的东西,把更多底层的事情交给底层的工具来完成,何苦要勉强自己?

但笔者觉得,这个道理就像“我会自己做饭,但我可以不用做(有人给我做)”,和“我不会做饭,只能指望别人给我做”是完全不同的两种状态。尽管工具可以提供优化,但“我很清楚底层原理,了解他们是如何优化的,然后我可以屏蔽很多底层的东西,使用方便的工具来提升我的工作效率”和“我根本不知道底层原理,只能没心没肺地用工具”也是不同的状态。笔者希望把这些告诉读者,这样即便工具出现一些问题的时候,我们也能有一个定位思路,而不会束手无策。

C++11和C++20

前面章节中笔者提到,C++的迭代过程中,主要是通过STL提供更方便的工具来解决原始缺陷的。但也有例外,C++11和C++20就是非常具有代表性的2次更新。

C++11引入的「自动类型推导」「右值引用」「移动语义」「lambda表达式」「强枚举」「基于范围的for循环」「变参模板」「常量表达式」等等的特性,其实都是对C++语言的一种扩充。C++11推出后,立刻让人感觉C++不再是C的感觉了。

只不过,兼容性是C++更多用于考虑的,一方面是出于对老项目迁移的门槛考虑,另一方面是对编译器运行方式的考虑,它并没有做过多的“改正”,而是以“修补”为主。举例来说,虽然引入了lambda表达式,但并没有用它代替函数指针,代替仿函数类型。再比如虽然引入了常量表达式,但仍然保留了const关键字的性质,甚至还做了向下兼容(比如前面章节提到的给常量表达式取地址后,会变为只读变量)。

之后的C++14、C++17更多的是在C++11的基础上进行了完善,因为你能够感觉到,这两个标准虽然提供了新的内容,但从根本上来说,它仍然是C++11的理念。比如C++14可以用auto推导函数返回值,但它并没有改变“函数返回值必须确定”这一理念,所以返回多种类型的时候只会以第一个为准。再比如C++17中引入了「折叠表达式」以及由「合并using」所诞生的很多奇技淫巧,让模板元编程更上一层楼,但它并没有解决模板元编程的本质是利用「SFINAE」,所以如果匹配失败,编译器报错会充斥非常复杂的SFINAE过程,导致开发者没法快速获取核心信息。

在这里举个小例子,假如我想判断某个类中是否含有名为Find、空参且返回值为int方法,如果有就可以传入Process函数中,那么用C++17的方法应该这样写:

template <typename T, typename R = void>
struct HasFind : std::false_value {}

template <typename T>
struct HasFind<T, typename std::void_t<decltype(&T::Find)>>
    : std::disjunction<std::is_name<decltype(&T::Find), int (T::*)(void)>,
      std::disjunction<std::is_name<decltype(&T::Find), int (T::*)(void) const>,
      std::disjunction<std::is_name<decltype(&T::Find), int (T::*)(void) noexcept>,
      std::disjunction<std::is_name<decltype(&T::Find), int (T::*)(void) const noexcept>
    > {};

template <typename T>
auto Process(const T &t) -> std::enable_if_t<HasFind<std::remove_reference_t<T>>::value, void> {
}

首先要想着把T::Find抠出来,对它进行decltype,如果这个操作是合法的,就说明T中含有这个成员,因此就能利用SFINAE原则匹配到下面HasFind的特例,否则匹配通用模板(也就是false_value了)。

其次,针对含有成员Find的类型再继续进行其类型判断,让它必须是一个返回值为int且空参的非静态成员函数,此时还不得不考虑constnoexcept的问题。

最后再利用std::enable_if进行判断类型是否匹配,在其内部其实仍然利用的是SFINAE原则,对于匹配不上的类型通过“只声明,不定义”的方式让它不能通过编译。

template <bool conj, typename T>
struct enable_if; // 没有实现,所以会编译不通过

template <typename T>
struct enable_if<true, T> {
  using type = T;
}; // 当第一个参数是true的时候才能编译通过,并且把T传递出来

用上例是想表明,尽管C++17提供了方便的工具,但依然逃不过“利用SFINAE匹配原则”来实现功能的理念,这一点就是从C++11继承来的。

而C++20的诞生又是一次颠覆性的,它引入的「concept」则是彻彻底底改变了这一行为,让类似于“限定模板类型”的工作不再依靠SFINAE匹配。比如上面用于判断Find方法功能,在C++20时可以写成这样:

template <typename T>
requires requires (T t) {
    {t.Find()} -> std::same_as<int>;
}
void Process(const T &t) {
    std::cout << 123 << std::endl;
}

其中的类型约束条件就可以定义成一个concept,所以还可以改写成这样:

template <typename T>
concept HasFind = requires (T t) {
    {t.Find()} -> std::same_as<int>;
};

template <typename T>
requires HasFind<T>
void Process(const T &t) {
    std::cout << 123 << std::endl;
}

可以看出,这样就是彻底在“语言”层面解决“模板类型限制”的问题。这样一来语法表达更加清晰,报错信息也更加纯粹(不会出现一大堆SFINAE过程)。

因此我们说,C++20是C++的又一次颠覆,就是在于C++20不再是一味地通过扩充STL的功能来“找补”,而是从语言维度出发,真正地“进化”C++语言。

除了concept外,C++20还提供了「model」概念,用于优化传承已久的头文件编译方式,这同样也是从语言的层面来解决问题。

由于C++20在业内并没有普及,因此本文主要介绍C++17下的C++缺陷和思考,并且以“思考”和“底层原理”为主,因此不再过多介绍语言特性。如果有读者希望了解各版本C++新特性,以及C++20提出的新理念,那么可以期待笔者后续将会编写的其他系列的文章

一些方便的工具

【说明:其实我本来没想写这一章,因为主要本文以“思考”和“底层原理”为主,但鉴于读者们强烈要求,最终决定在截稿前补充这一章,介绍一些用于避坑的工具,还有一些触发缺陷的代替写法,但仅做非常的简单介绍,有详细需求的读者可以期待我其他系列文章。】

智能指针

智能指针是一个用来代替newdelete的方案,本质是一个引用计数器。shared_ptr会在最后一个指向对象的指针释放时析构对象。

void Demo() {
  auto p = std::make_shared<Test>(1, 2);
  {
  	auto p2 = p; // 引用计数加1
  } // p2释放,引用计数减1
} // p释放,p目前是最后一个指针了,会析构对象

unique_ptr就是独立持有,只支持转交,不支持复制:

void Demo() {
  auto p = std::make_unique<Test>(1, 2);
  auto p2 = p; // ERR,unique指针不能复制
  auto p3 = std::move(p); // OK,可以转交,转交后p变为nullptr,不再控制对象
}

weak_ptr主要解决循环引用问题:

struct Test2;
struct Test1 {
  std::shared_ptr<Test2> ptr;
};

struct Test2 {
  std::shared_ptr<Test1> ptr;
};

void Demo() {
  auto p1 = std::make_shared<Test1>();
  auto p2 = std::make_shared<Test2>();
  p1->ptr = p2;
  p2->ptr = p1;
}; // p1和p2释放了,但是Test1对象内部的ptr和Test2对象内部的ptr还在互相引用,所以这两个对象都不能被释放

因此要将其中一个改为weak_ptr,它不会对引用计数产生作用:

struct Test2;
struct Test1 {
  std::shared_ptr<Test2> ptr;
};

struct Test2 {
  std::weak_ptr<Test1> ptr;
};

void Demo() {
  auto p1 = std::make_shared<Test1>();
  auto p2 = std::make_shared<Test2>();
  p1->ptr = p2;
  p2->ptr = p1;
}; // 可以正常释放

string_view

使用string主要遇到的问题是复制,尤其是获取子串的时候,一定会发生复制:

std::string str = "abc123";
auto substr = str.substr(2); // 生成新串

另外就是string是非平凡的,因此C++17引入了string_view,用于获取字符串一个切片,它是平凡的,并且不会发生文本的复制:

std::string_view sv = "abc123"; // 数据会保留在全局区,string_view更像是一组指针
auto substr = sv.substr(2); // 新的视图不会复制原本的数据

tuple

tuple可以理解为元组,或者是成员匿名的C风格结构体。可以比较方便地绑定一组数据。

std::tuple tu(1, 5.0, std::string("abc"));
// 获取内部成员
auto &inner = std::get<1>(tu);
// 全量解开
int m1;
double m2;
std::string m3;
std::tie(m1, m2, m3) = tu;
// 结构化绑定
auto [d1, d2, d3] = tu;

用做函数返回值也可以间接做到“返回多值”的作用:

using err_t = std::tuple<int, std::string>;

err_t Process() {
  if (err) {
    return {err_code, "err msg"};
  }
  return {0, ""};
};

这里比较期待的是能用原生语法支持,比如说像Swift中,括号表示元组

// 定义元组
let tup1 = (1, 4.5, "abc")
var tup2: (Int, String)
tup2.1 = "123"
let a = tup2.1

// 函数返回元组
func Process() -> (Int, String) {
  return (0, "")
}

optional

optional用于表示“可选”量,内含“存在”语义,不用单独选一个量来表示空:

void Demo() {
	std::optional<int> oi; // 定义
	oi = 5; // 赋值
	oi.emplace(8); // 赋值
	oi.reset(); // 置空
	if (io.has_value()) { // 判断有无
		int val = oi.value(); // 获取内部值
	}
}

还是跟Swift比较一下,因为Swift原生支持可选类型,语法非常整洁:

var oi : Int? // 定义可选Int类型
oi = 5 // 赋值
oi = nil // 置空
if (oi == nil) {
	let val = oi! // 解包
}

class Test {
  func f() -> Int {}
}
var obj: Test!
let i = obj?.f() // i是可选Int型,如果obj为空则返回nil,否则解包后调用f函数
let obj2 = obj ?? Test() // obj2是Test类型,如果obj为空则返回新Test对象,否则返回obj的解包

所以同样期待可选类型能够被原生语法支持

总结与感悟

与C++的初见

想先聊聊笔者个人的经历,当年我上大学的时候一心想做iOS方向,所以我的启蒙语言是OC。曾经的我还用OC去批判过C++的不合理。

后来我想做一个小型的手游,要用到cocos2d游戏引擎,cocos2d原本就是OC写的,但由于OC仅仅能用在iOS上,不能移植到Android,因此国内几乎找不到OC版cocos2d的任何资料。唯一可用的就是官方文档,但官方文档的缺点就是,它是一个类似于字典的资料,你首先要知道你要查什么,才能上去查。但是对于一个新手来说,更需要的是一个向导,告诉你怎么上手,怎么写个hello world,有哪些基础组件分别怎么用,展示几个demo这种的资料。但OC版的恰好没有,有入门资料的只有cocos2d-x(C++移植版)、cocos2d-js和cocos2d-lua。其中C++版的资料最多,于是我当时就只能读C++版的资料。

但早期版本的cocos2d-x属于OC向C++的移植版,命名、设计理念等都是跟OC保持一致的,所以那时候你读cocos2d-x的资料,然后再去做OC版原生cocos2d的开发是没什么问题的。但我当年非常不赶巧,我正好赶上那一版的cocos2d-x做C++化的改造。比如引入命名空间,把cclayer变成了cocos2d::layer;比如做STL移植,把CCString迁移成std::string,把CCMap迁移成std::map;再比如设计方式上,把原本OC的init函数改成了C++构造函数selector改成了std::function,诸多仿函数工具都转换为了lambda展现。所以那一版本的cocos2d-x我根本读不懂,要想读懂,就得先学会C++。后来考虑到反正C++和OC是可以混编的,干脆直接用C++版的cocos2d来做开发算了。我就这样糊里糊涂地学起了C++。

但这种孽缘一旦开始,就很难再停下来了。随着我对C++的不断深入学习,我逐渐发现C++很有趣,而且正是因为它的复杂,让我有了持续学下去的动力。每当我以为我差不多征服了C++的时候,我就总能再发现一些我没见过的语法、没踩过的坑,然后就会促使我继续深入研究它。

一段优越感极强的阶段

我在上一家公司曾经做过一段时间的交换机嵌入式开发,原本那就是纯C的开发(而且还是C89标准),后来公司全面普及编程能力,成立了一个先锋队,尝试向C++转型。我当时参与并且主导了其中一个领域,把C89改造成C++14。

那时的一段时间,我对“自己会使用C++”这件事有着非常强的优越感,而且,时不时会炫耀自己掌握的C++的奇技淫巧。而且那段时间我挂在嘴边最多的一句话就是“不是这玩意不合理,是你不会用!”。那个时候根本不想承认C++存在缺陷,或者哪里设计不合理。在我心目中,C++就是最合理的,世界上最好的编程语言。其他人觉得有问题无非就是他没有掌握,而自己掌握了其他人觉得复杂的事情,就不得不产生了非常强的优越感。

所以我曾经觉得C++就是我的信仰,只有C++程序员才是真正的程序员,你们其他语言的懂指针吗?懂模板吗?看到那一大串模板套模板的时候你不晕菜吗?哈哈!我不仅能看懂,我还能自己手撸type_traits,了不起吧?

所以那个时期,其实是自己给自己设置了一道屏障,让自己不再去接触其他领域的内容,得意洋洋地满足于一个狭窄的领域中。可能人就是这样,会有一段新鲜时期,过后就是一段浮躁期,但最后还是会沉下来,进入冷静期。而到了冷静期,你又会有非常不同的视野。

冷静期后

我逐渐发现,身边很多同学、朋友都“叛逃”了C++,转向了其他的(比如说Go),或许确实是因为C++的复杂造成了劝退,但我觉得,需要思考一下,为什么会这样。

他们很多人都说Go是“下一个C++”,我原本并不认同,我认为C++永远都会作为一个长老的形象存在,其他那些“年轻人(新语言)”还没有经历时间的打磨,所以不以为然。但后来我慢慢发现,这话虽然不全对,但在一些情况下是有道理的。比如互联网公司与传统软件公司不同,更多的项目都是没有特别久的分析和设计时间,所以要求快速迭代。但C++其实并不是特别适合这种场景,尽管语言只是语言,设计才是关键,但语言也是一种工具,也有更合适的场景。

而对于Go来说,似乎更适合这种微服务的领域,我就是开发一个领域内的功能,然后对外一共一个rpc接口。那其实这种模式下,我似乎并不需要太多的OOP设计,也不需要过分考虑比如一个字符串复制所带来的性能损耗。但如果使用了C++,你不得不去考虑复制问题、平凡析构问题、内存泄漏问题等等的事情,我们能专心投在核心领域的精力就会分散。

所以之后的一段时间我学习了一些其他的语言,尤其是Go语言,我当时看的那本Go语言的资料,满篇都在有意无意地跟C++进行比较,有的时候还用C++代码来解释Go的语言现象。那个时候我就思考,Go的这种设计到底是为了什么?它比C++强在哪里?又弱在哪里?

其实结论也是很简单的,就是说,C++是一种全能语言,而针对于某个更专精的领域,把这部分的功能加强,受影响的缺陷减弱或消除,然后去创造一个新的语言,更加适合这种场景的语言,那自然优势就是在这种场景下更加高效便捷。缺点也是显而易见的,换个领域它的特长就发挥不出来了。说通俗一点就是,C++能写OS、能写后端、还能写前端(Qt了解一下!),写后台可能拼不过Go,但Go你就写不了OS,写不了前端。所以这就是一个「通用」和「专精」的问题。

总结

曾经有很多朋友问过我,C++适不适合入门?C++适不适合干活?我学C++跟我学java哪个更赚钱啊?笔者持有这样的观点:C++并不是最适合生产的语言,但C++一定是最值得学习的语言。如果说你单纯就是想干活,享受产出的快乐,那我不建议你学C++,因为太容易劝退,找一些新语言,语法简单清晰容易上手,自然干活效率会高很多;但如果你希望更多地理解编程语言,全面了解一些自底层到上层的原理和进程,希望享受研究和开悟的快乐,那非C++莫属了。掌握了C++再去看其他语言,相信你一定会有不同的见解的。

所以到现在这个时间点,应该说,C++一样是我的信仰,我认为C++将会在将来很长一段时间存在,并且以一个长老的身份发挥其在业界的作用和价值,但同时也会有越来越多新语言的诞生,他们在自己适合的地方发挥着不一样的光彩。我也不再会否认C++的确有设计不合理的地方,不会否认其存在不擅长的领域,也不会再去鄙视那些吐槽C++复杂的人。与此同时,我也不会拒绝涉足其他的领域,我认为,只有不断学习比较,不断总结沉淀,才能持续进步。

如果你能读到这里的话,那非常感激你的支持,听我说谢谢你,因为有你……咳咳~。这篇文章作为我学习C++多年的一个沉淀,也希望借此把我的想法分享给读者,如果你有任何疑问或者建议,欢迎评论区留言!针对更多C++的特性的用法、编程技巧等内容,请期待我其他系列的文章

【完结】

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐