C++还是C,这是个问题

注:本文的作者是C++之父,我就是个翻译,不承担任何责任,对本文内容有异议请找作者


我一直在一个可以装逼的群里呆着,这群里经常会来很多萌新。他们总是要学C语言。本身我对C语言没啥意见,但是我要装逼啊,所以我就要和他们讨论,在我和善的和他们讨论想干啥的时候,发现他们想干的事完全可以用更加优美的现代语言解决,甚至跟C语言没半毛钱关系。有的程序是用C++比较好,所以有时候我就说了,直接学C++得了呗。他们发着北酱的表情,摇头说不行,因为网上说学C++必须先学C。我就特别装逼的跟他们说:

恭喜你碰见傻哔了

事实很明显不是这样的,稍有常识的人都能看出,基本你随便买一本C++的入门读物,都不要求你有什么鬼的C语言基础。所以今天我说的不是这事,总有人是C语言的脑残粉,然后各种攻击C++。一会说C++不适合初学者,一会说C++坑多,我就很不满意,C++他爹也很不满意。我俩一致认为:C++编程可比C简单多了

C是C++真子集。但并不是最易学的子集。C++是C语言的扩充,至今也是C语言最优秀的扩充(发展到C++11以及将来的C++17,很可能没有之一,顺带说一下,clang已经基本支持了已经颁布的C++17特性),C++在添加了诸多二十一世纪的语言特性之后,依然保持着可以匹敌C语言的效率。C缺少许许多多现代开发的必备特性,而且标准库比较原始,设计的差劲。而使用 C++的一些特性,加上经过诸多推敲的标准库可以让简单的工作保持简单,而不是像C语言那样,有的时候把简单的工作搞成一团乱麻。


简单还是复杂

我们来看一个电子邮件的栗子,老板来了,让我们写程序,很傻哔的程序,输入一个邮件名称和邮件服务器,返回一个邮件地址。假如我是个C厨,我要这么写:

char* compose(const char* name, const char* domain)
{
  char* res = malloc(strlen(name)+strlen(domain)+2); // space for strings, '@', and 0
  char* p = strcpy(res,name);
  p += strlen(name);
  *p = '@';
  strcpy(p+1,domain);
  return res;
}

 
 

C++的爹跟我在一个公司,他用C++,是这么写的

string compose(const string& name, const string& domain)
{
  return name+'@'+domain;
}

有人说初学者就适合学C,你摸着良心说一说,以上是用同样的抽象逻辑实现同样的功能的等价代码,如果你是初学者,你愿意学哪个呢?另外,你猜猜我写的C语言版本完全正确吗?:P。如果在学C++之前学C,那么就意味着要在学C++之前再次忍受C的设计缺陷,而这些缺陷大多都是C++中已经避免了的。

最后, 你猜那个版本性能好? 当然是C++版的。因为不需要计算字符串的长度,也不需要在堆上分配空间。


新特性只有大牛才用

有许多人初学C++时使用的是VC6,那时候没有什么好用的IDE,可以理解。但是现在他们依然这么认为,这就不对了。当我和他们说这件事的时候,他们告诉我:“初学者根本用不上C++的新特性,不如先用VC6打好基础”这是对的吗?

有了 C++11 之后, C++并没有变得更复杂,而是变得更易入门。举个例子, 这是标准库 vector 的初始化方式。

vector<int> v = {1,2,3,5,8,13};
C++98 时代, 我们只能用列表初始化数组, 而 C++11 时代, 我们可以定义一个构造器,接受 {} initializer。

我们也可以用 range-for 遍历 vector。

for(int x : v) test(x);
C++11 的目标之一, 就是让简单的归于简单, 同时不引起性能过载,也就是前面说的,和C匹敌的性能。


C语言比C++的效率更高

C语言可以比C++更加精确的对硬件进行控制,但这是在牺牲了抽象能力的基础上。但在很多时候,抽象比精确控制更加重要,它可以让你的项目变得更易懂。于是有很多人说,由于C++的过度抽象,已经牺牲了效率,现在C++比C慢很多,但是事实真的是这样吗?
大多数人都认为高性能的代码一定得是底层的. 甚至有人认为底层代码就代表高性能(如果一段代码很丑, 就一定跑得快! 因为人家花了大量的时间和精力来写这段丑得不常规的代码)。但事实并非如此,虽然有些硬件资源确实得用底层代码访问. 但是, 千万不要陷入自欺欺人的骗局,你要自己去测试一下, 看看你的花费大量时间进行所谓的“优化”是否值得。现代的 C++ 编译器十分高效。对于一个生活在二十一世纪的程序员,底层代码最好隐藏在一个良好设计的接口背后, 以方便使用。通常情况下, 将底层封装后, 也有利于更好的优化性能(比如防止了滥用特性)。我们要先尝试先向高级特性要性能, 不要一开始就陷入位操作和指针中。下面我们来看一个经典例子:


老板又来了,又给了我和C++爹一个傻哔的任务:降序排列浮点数。我俩都一致认为这个问题不需要重复造轮子,因为无论是C还是C++,都提供了经过了特殊优化的排序算法。很好,于是作为C厨的我先写了我的C语言版本:

int greater(const void* p, const void* q)  // three-way compare
{
  double x = *(double*)p;  // get the double value stored at the address p
  double y = *(double*)q;
  if (x>y) return 1;
  if (x<y) return -1;
  return 0;
}

void do_my_sort(double* p, unsigned int n)
{
  qsort(p,n,sizeof(*p),greater);
}

int main()
{
  double a[500000];
  // ... fill a ...
  do_my_sort(a,sizeof(a)/sizeof(*a));  // pass pointer and number of elements
  // ...
}
如果你学C学的不是很好,我可以给你讲解一下qsort这个函数,这个函数需要四个参数:

  • 一个指针, 指向二进制序列
  • 元素的个数
  • 元素的大小
  • 一个比较函数, 参数是两个指针
我们不是在排列普通数据,而是在排列double。但是qsort是不知道我们在排序什么的。所以我不仅要提供比较double的方法(一个接受两个指针的函数指针),还要提供double的数据大小!偶买噶,难道编译器真的不知道double多大?不,他知道的,但是由于qsort的底层接口设计问题使得编译器完全不能利用这些类型信息。必须显示声明这些弱智的信息也给造成错误打开了大门。那两个整数参数就容易搞混位置, 要是真的搞混了位置, 编译器又不可能提醒你。如果你写错了,你真的是不会吗?并不是,可能你只是一不小心,但这么一下子放在大型项目里就可能引入一个难以察觉的BUG——谁知道问题居然出现在一个小小的比较函数里呢。这时候可能会有人跳出来指责我:你写错了因为你不是一个合格的C程序员。难道一个合格的C程序员就要纠结于这样的细枝末节问题吗?或许这就是传说中的心智包袱。如果你去看看一些为了避免这种心智包袱而出现的 qsort的其它实现, 你会发现为了补偿信息缺失, 库函数的作者们真的很努力,他们利用各种hack来给这个语言层面的设计失误打补丁。但又会引入新的问题, 交换两个元素的通用算法和交换两个 double的复杂度不可同日而语, 效率也大大降低。比较函数的开销也只能在编译器对函数指针做了常量增值之后消失。

下面我们来看一下C++爹写的C++版本。

void do_my_sort(vector<double>& v)
{
  sort(v,[](double x, double y) { return x>y; });  // sort v in decreasing order
}
 
int main()
{
  vector<double> vd;
  // ... fill vd ...
  do_my_sort(v);
  // ...
}

vector知道自己的size, 所以, 没要必要明说元素的个数。我们从未丢失过元素的类型,也就不需要处理元素类型数据大小。默认情况下,sort() 使用升序排列, 所以我们要指定排序方式, 就如同在qsort()中做的那样。在这里使用了一个lambda。据我所知,所有的C++编译器都会在这里内联这个函数,所以也就是一个大于比较的机器指令,不再需要低效的函数调用。在这个例子里使用了一个容器版本的sort() 可以不必显示指定迭代器。也就是说,不用这样:

std::sort(v.begin(),v.end(),[](double x, double y) { return x>y; });
哪个版本更快?不同编译器和不同的库,给出的结果大多是不同的。就最近运行例子的结果来讲, sort() 版本的速度是 qsort()版本的2.5倍。你也许会说是编译器的问题, 但我从未见过 qsort() 打败了 sort()。我曾经见过 sort()qsort() 快10倍的。怎么会这样? C++标准库的 sort 明显更高层, 更通用, 更柔软。它是类型安全的, 可以对存储类型、元素类型和排序方法偏特化。没有指针, 强制类型转换和位操作。C++标准库真的很努力,这个新的实现没有丢失什么信息,而且提高了效率,还使得内联和优化非常方便。想必到了这里,你可以体会到设计的力量: 优秀的设计可以有效的在初期规避很多问题,而糟糕的设计引入的问题不是后来缝缝补补就能解决的

C和C++的性能不同是一种误解。即使我们退一步来讲,如果你使用同样的抽象和同样的逻辑去实现同样的代码,C和C++的性能几乎是严格相等的。如果你需要抽象,例如跳转表,那和同样能力的虚函数是没有差别的,甚至编译器能够更好的优化使用虚函数的代码。
或许C++会鼓励你使用运行效率更低,但是开发效率更高的抽象(例如使用相对复杂的Function Object),但是你可以选择不用。更多的,C++的还会有诸如内联函数之类的机制来改善效率。

猜你喜欢

转载自blog.csdn.net/FYZDMMCpp/article/details/47029981