Please enable JavaScript to view the comments powered by Disqus.

C与C++的不同

UPDATE 2017/03/10: 填了一部分

由于本人才疏学浅造成的错误希望各位在评论中心平气和地指正。


auto 关键字

在C语言中,这个关键字用于声明变量的生存期为自动,即将不在任何类、结构、枚举、联合和函数中定义的变量视为全局变量,而在函数中定义的变量视为局部变量。这个关键字不怎么多写,因为函数内局部变量默认就是auto的,与程序员预期相符合。C++98标准中auto关键字与C语言中的相同;而自C++11以来,auto的语义被修改,原来代表 “automatic” 的语义被废弃,该关键字转而用于两种情况:声明变量时根据初始化表达式自动推断该变量的类型、声明函数时函数返回值的占位符。

C++ auto用法举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <vector>

template <class T, class U>
auto add(T t, U u) -> decltype(t + u) // add的返回类型与operator+(T, U)的相同
{
return t + u;
}

std::vector<type> func(){
//something here
}

auto a = func(); // a的类型与func的返回值类型相同

之所以上面返回类型需要使用auto占位,是因为你的类型推导式用到了函数参数tu,而编译器读到返回类型推导式decltype(t + u)的时候tu还没声明,也就用不了,形成了循环依赖。除了这种格式,还可以使用decltype(*(T*)0 + *(U*)0)这样的格式,虽然更难看。

类型别名

类型别名用于避免又臭又长的变量声明(比如函数指针),以及保证可移植性(比如stdint.h里的int32_t保证是32位带符号整型)等等情况下。

C和C++均支持typedef的类型别名,并且都不允许typedef类型别名声明中出现static. typedef声明的作用域如同一个变量声明,在C++中多了类从而类中的typedef声明也多了访问权限控制。

在C++中多出了模板,而typedef不支持模板,定义的类型别名只能是模板实例化之后的类型。若要对模板进行类型别名声明新的模板,就要使用using关键字。

using的使用方式如下:

1
2
3
4
5
6
// Simple using alias
using alias_name = type_id;

// For template alias
template <template-parameter-list>
using alias_name = type_id;

举个例子:

1
2
3
4
5
6
7
template<class T>
struct Alloc { };

template<class T>
using Vec = vector<T, Alloc<T>>; // type-id is vector<T, Alloc<T>>

Vec<int> v; // Vec<int> is the same as vector<int, Alloc<int>>

布尔类型

C在C99标准之前没有内置布尔类型,因此只能通过typedef int bool的方式模拟;C99及以后在C语言中引入了布尔类型,其关键字是_Bool. 如果想要像C++那样使用布尔类型的话,包含头文件stdbool.h.

C++自带布尔类型,关键字是bool.

数组

就定长数组(编译期可确定其长度)而言,C和C++毫无区别。而C99引入了变长数组(Variadic Length Array, VLA)之后,一切就变得不一样了。

就行为上来说,VLA相当于自动帮你使用了mallocfree, 只是开辟的空间在栈上罢了。同时对数组引入了新的语法(一般用在函数声明的参数列表里面),来描述新引入的VLA. 比如int[*]代表这是一个int的VLA, int[static 10]代表这是一个至少有10个int的VLA(该语法有利于优化).

C++中没有VLA, 如果一定要长度可变的数组那么使用std::vector; 要封装原生数组使用C++11引入的std::array.

泛型

这里的泛型是针对C11引入的_Generic的直译。

在C11之前,我们无法在C语言中写一个针对所有数据类型都可用的算法(比如针对任何类型的数组都可用的排序函数),所能做的只有全部转换成void*然后提供一个自定义的处理函数,通过这样拐弯抹角的方式来实现泛型。一个典型的例子就是stdlib.h里的qsort, 其原型定义如下:

1
2
void qsort( void *ptr, size_t count, size_t size,
int (*comp)(const void *, const void *) );

在C11中提供了_Generic的功能,它能根据传入参数的类型来决定对应的操作。如下就是一个_Generic的例子,可以根据传入的参数到底是long double还是float来决定调用的函数到底是cbrtl还是cbrtf. 如果提供的类型无一满足,那么会使用default条目定义的规则。

1
2
3
4
5
#define cbrt(X) _Generic((X), \
long double: cbrtl, \
default: cbrt, \
float: cbrtf \
)(X)

在C++中,语言自带的重载和模板特性就不知道比C高到哪里去了,虽然会有Name Mangling使ABI不稳定这种副作用。

struct

在C中,struct只能内含数据类型,不能包有函数和static成员。但C99之后也有一些C++没有的特性,比如Flexible array member(其实这个特性在进标准之前就有很多类似手法了),compound literaldesignated initialization(这两个不光可用于struct)。

1
2
3
4
5
6
7
8
typedef struct test {
int i;
char c;
} test;
// compound literal
test t1 = (test){0, '0'};
// designated initialization
test t2 = {.c = '0', .i = 0};

这种缺乏导致C不能很方便地写出很“面向对象”的代码。成员函数和vtable可以手动添加struct的函数指针成员来实现,假装自己有OOP的样子;而OOP中的继承特性就不太好写了。方法、成员的继承可以通过复制粘贴来实现,但基类可以指向子类,虚函数这些语义怎么办呢?

为了保持与C的兼容,C++让简单的struct, class, union保持C风格,让它们能够与C传统的malloc, memmove等函数交互。

此外,在C中struct, union, enum的类型名称和变量名、函数名等等是区别对待的,即使二者重名也是可以的。因此typedef struct tag tag这样的语句很有必要,如果不写那么声明一个tag结构体一定要使用struct tag var而不是tag var. 在C++中你可以认为是自动插入了这样的typedef语句。

空指针

C语言中,任何类型的两个空指针值相等。宏定义NULL的值具体取决于实现。C99中规定NULL应该是从整数类型的0显式或隐式地转换而来。

C++出于兼容C的考虑保留了NULL, 其内容可能是:

  • 整数类型的右值(rvalue)常量表达式,该值为0 (C++11之前)
  • 整数类型的纯右值(prvalue)常量表达式,该值为0; 或是std::nullptr_t类型的纯右值 (C++11到C++14)
  • 值为0的整型字面值,或是或是std::nullptr_t类型的纯右值 (C++14起)

C++引入新的关键字nullptr来代表空指针,来解决之前0同时具有整数0和空指针语义的问题。例如如下的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <cstddef>
#include <iostream>

template<class F, class A>
void Fwd(F f, A a)
{
f(a);
}

void g(int* i)
{
std::cout << "Function g called\n";
}

int main()
{
g(NULL); // Fine
g(0); // Fine

Fwd(g, nullptr); // Fine
// Fwd(g, NULL); // ERROR: No function g(int)
}

在上面的程序中,Fwd(g,NULL)并不是如想象中一般地调用了g(int*),反而去寻找g(int)的重载。使用nullptr就不会出现这样的问题。

二者NULL最原始的宏定义都在stddef.h(或cstddef)里。

类型转换

C支持隐式和显式的两种转换,隐式的由编译器自动完成,显式的只有(type)expression这一种方式。

C++兼容C的类型转换方式,但它引入了对象机制后更加复杂。首先为了与C兼容保留了上面的转换方式,同时引入了等价的type(expression)方式。除此之外,还存在static_cast, const_cast, dynamic_cast, reinterpret_cast这四种。从理论上来说,(type) expressiontype(expression)都是以上几种XXXX_cast的组合。在C++中,类型转换推荐使用XXXX_cast的形式。

由于C++里面的构造函数和自定义转换函数,隐式转换可能会发生在各种意想不到的地方。关于类型转换的知识十分繁杂,具体详见C++隐式类型转换C隐式类型转换

指针和内存管理

这部分主要就是体现出C++强于C的地方了。涉及到动态内存管理,大家都只能向操作系统申请空间,到时候都得释放,但是C++能够玩出花来。利用类封装加上RAII机制,C++标准库实现了智能指针,一个是基于引用计数的shared_ptr, 另一个是独享资源所有权的unique_ptr, 利用这两样工具,基本能摆脱手动delete的烦恼。

异常处理

C语言没有什么方便的异常处理机制,一般是在每个可能出异常的地方套上个if-else, 异常来临时exit, 申请的资源通过在atexit注册的函数来处理。或者使用setjmplongjmp.

在C++中有现在满地都是的 try-catch 流,使用这个的时候需要时刻留心,保证代码的异常安全性。同时,在异常发生时使用原生指针管理的内存如果不在catch块处理那么内存就这么泄露了。

参考资料

auto部分:

  1. http://en.cppreference.com/w/cpp/language/auto

  2. http://en.cppreference.com/w/cpp/keyword

类型别名部分:

  1. http://en.cppreference.com/w/cpp/language/typedef

  2. http://en.cppreference.com/w/c/language/typedef

  3. http://en.cppreference.com/w/cpp/language/type_alias

布尔类型部分:

  1. https://en.wikipedia.org/wiki/C99

泛型部分:

  1. http://en.cppreference.com/w/c/language/generic

空指针部分:

  1. https://en.wikipedia.org/wiki/Null_pointer

  2. http://en.cppreference.com/w/cpp/language/nullptr

  3. http://en.cppreference.com/w/cpp/types/NULL

类型转换部分:

  1. http://stackoverflow.com/a/332086
作者:Dr. A. Clef
发布日期:2015-12-19
修改日期:2017-06-10
发布协议: BY-SA