编译环境:使用 clang++ 作为唯一使用的编译器,同时总是在代码中使用 -std=c++2a 编译标志。
> clang++ -v Apple LLVM version 10.0.1 (clang-1001.0.46.4) Target: x86_64-apple-darwin18.6.0 Thread model: posix InstalledDir: /Library/Developer/CommandLineTools/usr/bin
// foo.h #ifdef __cplusplus extern "C" { #endif int add(int x, int y); #ifdef __cplusplus } #endif // foo.c int add(int x, int y) { return x+y; } // 1.1.cpp #include "foo.h" #include #include int main() { [out = std::ref(std::cout << "Result from C code: " << add(1, 2))](){ out.get() << "."; }(); return 0; }
gcc -c foo.c
clang++ 1.1.cpp foo.o -std=c++2a -o 1.1
C = gcc CXX = clang++ SOURCE_C = foo.c OBJECTS_C = foo.o SOURCE_CXX = 1.1.cpp TARGET = 1.1 LDFLAGS_COMMON = -std=c++2a all: $(C) -c $(SOURCE_C) $(CXX) $(SOURCE_CXX) $(OBJECTS_C) $(LDFLAGS_COMMON) -o $(TARGET) clean: rm -rf *.o $(TARGET)
[out = std::ref(std::cout << "Result from C code: " << add(1, 2))](){ out.get() << "."; }();
- C++ 语言导学. Bjarne Stroustrup
- C++ 历史
- C++ 特性在 GCC/Clang 等编译器中的支持情况
- C++98 与 C99 之间的区别许可本书系欧长坤著,采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议许可。项目中代码使用 MIT 协议开源,参见许可。
![6c2fe6ac86979c82cfa03550c66c88f7.png](https://i-blog.csdnimg.cn/blog_migrate/be1cc00427cf9e5efe7a0b7fbc862803.jpeg)
进一步阅读的参考文献
不必担心,本书的后续章节将为你介绍这一切。
如果你是首次接触现代 C++,那么你很可能还看不懂上面的那一小段代码,即:
注意:Makefile 中的缩进是制表符而不是空格符,如果你直接复制这段代码到你的编辑器中,制表符可能会被自动替换掉,请自行确保在 Makefile 中的缩进是由制表符完成的。
如果你还不知道 Makefile 的使用也没有关系,本教程中不会构建过于复杂的代码,简单的在命令行中使用 clang++ -std=c++2a 也可以阅读本书。
当然,你可以使用 Makefile 来编译上面的代码:
编译出 foo.o 文件,再使用 clang++ 将 C++代码和 .o 文件链接起来(或者都编译为 .o 再统一链接):
应先使用 gcc 编译 C 语言的代码:
从现在开始,你的脑子里应该树立『C++ 不是 C 的一个超集』这个观念(而且从一开始就不是,后面的进一步阅读的参考文献中给出了 C++98 和 C99 之间的区别)。在编写 C++ 时,也应该尽可能的避免使用诸如 void* 之类的程序风格。而在不得不使用 C 时,应该注意使用 extern "C" 这种特性,将 C 语言的代码与 C++代码进行分离编译,再统一链接这种做法,例如:
出于一些不可抗力、历史原因,我们不得不在 C++ 中使用一些 C 语言 中使用一些 C 语言代码(甚至古老的 C 语言代码),例如 Linux 系统调用。在现代 C C 与 C++ 的区别是什么时,普遍除了回答面向对象的类特性、泛型编程的模板特性外,就没有其他的看法了,甚至直接回答『差不多』,也是大有人在。图 1.2 中的韦恩图大致上回答了 C 和 C++ 相关的兼容情况。
![9529f39a61bb35eb91ad2ba672de60e2.png](https://i-blog.csdnimg.cn/blog_migrate/a88daa4c039da85c2cb29fcb3b9b1c8f.jpeg)
与 C 的兼容性
还有一些其他诸如参数绑定(C++11 提供了 std::bind 和 std::function)、export 等特性也均被弃用。前面提到的这些特性如果你从未使用或者听说过,也请不要尝试去了解他们,应该向新标准靠拢,直接学习新特性。毕竟,技术是向前发展的。
- 不再允许字符串字面值常量赋值给一个 char *。如果需要用字符串字面值常量赋值和初始化一个 char *,应该使用 const char * 或者 auto。 char *str = "hello world!"; // 将出现弃用警告
- ……等等
- 特别地,在最新的 C++17 标准中弃用了一些可以使用的 C 标准库,例如 、、 与 等
- C 语言风格的类型转换被弃用(即在变量前使用 (convert_type)),应该使用 static_cast、reinterpret_cast、const_cast 来进行类型转换。
- 如果一个类有析构函数,为其生成拷贝构造函数和拷贝赋值运算符的特性被弃用了。
- bool 类型的 ++ 操作被弃用。
- register 关键字被弃用,可以使用但不再具备任何实际含义。
- auto_ptr 被弃用,应使用 unique_ptr。
- C++98 异常说明、 unexpected_handler、set_unexpected() 等相关特性被弃用,应该使用 noexcept。
注意:弃用并非彻底不能用,只是用于暗示程序员这些特性将从未来的标准中消失,应该尽量避免使用。但是,已弃用的特性依然是标准库的一部分,并且出于兼容性的考虑,大部分特性其实会『永久』保留。
在学习现代 C++ 之前,我们先了解一下从 C++11 开始,被弃用的主要特性:
常量
nullptr
nullptr 出现的目的是为了替代 NULL。在某种意义上来说,传统 C++ 会把 NULL、0 视为同一种东西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义为 0。
C++ 不允许直接将 void * 隐式转换到其他类型。但如果编译器尝试把 NULL 定义为 ((void*)0),那么在下面这句代码中:
char *ch = NULL;
void foo(char*); void foo(int);
#include #include void foo(char *); void foo(int); int main() { if (std::is_same::value) std::cout << "NULL == 0" << std::endl; if (std::is_same::value) std::cout << "NULL == (void *)0" << std::endl; if (std::is_same::value) std::cout << "NULL == nullptr" << std::endl; foo(0); // 调用 foo(int) // foo(NULL); // 该行不能通过编译 foo(nullptr); // 调用 foo(char*) return 0; } void foo(char *) { std::cout << "foo(char*) is called" << std::endl; } void foo(int i) { std::cout << "foo(int) is called" << std::endl; }
foo(int) is called foo(char*) is called
#include #define LEN 10 int len_foo() { int i = 2; return i; } constexpr int len_foo_constexpr() { return 5; } constexpr int fibonacci(const int n) { return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2); } int main() { char arr_1[10]; // 合法 char arr_2[LEN]; // 合法 int len = 10; // char arr_3[len]; // 非法 const int len_2 = len + 1; constexpr int len_2_constexpr = 1 + 2 + 3; // char arr_4[len_2]; // 非法 char arr_4[len_2_constexpr]; // 合法 // char arr_5[len_foo()+5]; // 非法 char arr_6[len_foo_constexpr() + 1]; // 合法 std::cout << fibonacci(10) << std::endl; // 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 std::cout << fibonacci(10) << std::endl; return 0; }
constexpr int fibonacci(const int n) { return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2); }
constexpr int fibonacci(const int n) { if(n == 1) return 1; if(n == 2) return 1; return fibonacci(n-1) + fibonacci(n-2); }
constexpr int fibonacci(const int n) { return n == 1 || n == 2 ? 1 : fibonacci(n-1) + fibonacci(n-2); }
为此,我们可以写出下面这类简化的版本来使得函数从 C++11 开始即可用:
从 C++14 开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句,例如下面的代码在 C++11 的标准下是不能够通过编译的:
此外,constexpr 的函数可以使用递归:
C++11 提供了 constexpr 让用户显式的声明函数或对象构造函数在编译期会成为常量表达式,这个关键字明确的告诉编译器应该去验证 len_foo 在编译期就应该是一个常量表达式。
注意,现在大部分编译器其实都带有自身编译优化,很多非法行为在编译器优化的加持下会变得合法,若需重现编译报错的现象需要使用老版本的编译器。
上面的例子中,char arr_4[len_2] 可能比较令人困惑,因为 len_2 已经被定义为了常量。为什么 char arr_4[len_2] 仍然是非法的呢?这是因为 C++ 标准中数组的长度必须是一个常量表达式,而对于 len_2 而言,这是一个 const 常数,而不是一个常量表达式,因此(即便这种行为在大部分编译器中都支持,但是)它是一个非法的行为,我们需要使用接下来即将介绍的 C++11 引入的 constexpr 特性来解决这个问题;而对于 arr_5 来说,C++98 之前的编译器无法得知 len_foo() 在运行期实际上是返回一个常数,这也就导致了非法的产生。
C++ 本身已经具备了常量表达式的概念,比如 1+2, 3*4 这种表达式总是会产生相同的结果并且没有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。一个非常明显的例子就是在数组的定义阶段:
constexpr
此外,在上面的代码中,我们使用了 decltype 和 std::is_same 这两个属于现代 C++ 的语法,简单来说,decltype 用于类型推导,而 std::is_same 用于比较两个类型是否相等,我们会在后面 decltype 一节中详细讨论。
从输出中我们可以看出,NULL 不同于 0 与 nullptr。所以,请养成直接使用 nullptr的习惯。
将输出:
你可以尝试使用 clang++ 编译下面的代码:
为了解决这个问题,C++11 引入了 nullptr 关键字,专门用来区分空指针、0。而 nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
那么 foo(NULL); 这个语句将会去调用 foo(int),从而导致代码违反直觉。
没有了 void * 隐式转换的 C++ 只好将NULL 定义为 0。而这依然会产生新的问题,将 NULL 定义成 0 将导致 C++ 中重载特性发生混乱。考虑下面这两个 foo 函数:
![8bf6053f498a8d9a0e0afc728666e5e7.png](https://i-blog.csdnimg.cn/blog_migrate/184cf05660c6c4511367c05e8c82ef2c.jpeg)