C++的基本方法,在结构的内部放入函数。结构的这种新类型称为抽象数据类型(abstract data type),用这种结构创建的变量称为这个类型的对象(object)或实例(instance)。调用对象的成员函数称为向这个对象发消息(sending a message)。在面向对象的程序设计中的主要动作就是向对象发消息。
如果其中一个参数是另一个参数的负值,则调用abort()函数。Abort()的函数原型位于头文件cstdlib中,其典型实现是向标准错误流(即cerr使用的错误流)发送消息abnormal program termination(程序异常终止),然后终止程序。它还返回一个随实现而异的值,告诉操作系统(如果程序是由另一个程序调用的,则告诉父进程)处理失败。
多数计算机语言的输入和输出是以语言本身为基础实现的。但是C和C++都没有将输入和输出建立在语言中。这两种语言的关键字包括for和if,但不包括与I/O有关的内容。C语言最初把I/O留给了编译器实现人员。这样做的一个原因是为了让实现人员能够自由的设计I/O函数,使之最适合于目标计算机的硬件要求。实际上,多数实现人员都把I/O建立在最初为UNIX环境开发的库函数的基础之上。ANSI C 正式承认这个I/O软件包时,将其称为标准输入/输出包,并将其作为标准C库不可或缺的组成部分。C++也认可这个软件包,因此如果熟悉stdio.h文件中声明的C函数系列,则可以在C++程序中使用它们,较新的实现使用头文件cstdio来支持这些函数。
C++ 是一个用户群体相当大的语言。从 C++98 的出现到 C++11 的正式定稿经历了长达十年多之久的积累。
C++14/17 则是作为对 C++11 的重要补充和优化,C++20 则将这门语言领进了现代化的大门,所有这些新标准中扩充的特性,给 C++ 这门语言注入了新的活力。
那些还在坚持使用传统 C++ (本书把 C++98 及其之前的 C++ 特性均称之为传统 C++)而未接触过现代 C++ 的 C++ 程序员在见到诸如 Lambda 表达式这类全新特性时,甚至会流露出『学的不是同一门语言』的惊叹之情
现代 C++ (本书中均指 C++11/14/17/20) 为传统 C++ 注入的大量特性使得整个 C++ 变得更加像一门现代化的语言。现代 C++ 不仅仅增强了 C++ 语言自身的可用性,auto 关键字语义的修改使得我们更加有信心来操控极度复杂的模板类型。同时还对语言运行期进行了大量的强化,Lambda 表达式的出现让 C++ 具有了『匿名函数』的『闭包』特性,而这一特性几乎在现代的编程语言(诸如 Python/Swift/… )中已经司空见惯,右值引用的出现解决了 C++ 长期以来被人诟病的临时对象效率问题等等
C++17 则是近三年依赖 C++ 社区一致推进的方向,也指出了 现代C++ 编程的一个重要发展方向。尽管它的出现并不如 C++11 的分量之重,但它包含了大量小而美的语言与特性(例如结构化绑定),这些特性的出现再一次修正了我们在 C++ 中的编程范式。
现代 C++ 还为自身的标准库增加了非常多的工具和方法,诸如在语言自身标准的层面上制定了 std::thread,从而支持了并发编程,在不同平台上不再依赖于系统底层的 API,实现了语言层面的跨平台支持;std::regex 提供了完整的正则表达式支持等等。C++98 已经被实践证明了是一种非常成功的『范型』,而现代 C++ 的出现,则进一步推动这种范型,让 C++ 成为系统程序设计和库开发更好的语言。Concept 提供了对模板参数编译期的检查,进一步增强了语言整体的可用性。
出于一些不可抗力、历史原因,我们不得不在 C++ 中使用一些 C 语言代码(甚至古老的 C 语言代码),例如 Linux 系统调用。在现代 C++ 出现之前,大部分人当谈及『C 与 C++ 的区别是什么』时,普遍除了回答面向对象的类特性、泛型编程的模板特性外,就没有其他的看法了,甚至直接回答『差不多』,也是大有人在
从现在开始,你的脑子里应该树立『C++ 不是 C 的一个超集』这个观念(而且从一开始就不是,后面的进一步阅读的参考文献中给出了 C++98 和 C99 之间的区别)。
在编写 C++ 时,也应该尽可能的避免使用诸如 void* 之类的程序风格。而在不得不使用 C 时,应该注意使用 extern "C" 这种特性,将 C 语言的代码与 C++代码进行分离编译,再统一链接这种做法
// 将临时变量放到 if 语句内 if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end()) { *itr = 4; }
初始化列表
初始化是一个非常重要的语言特性,最常见的就是在对象进行初始化时进行使用。
在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组、 POD (Plain Old Data,即没有构造、析构和虚函数的类或结构体) 类型都可以使用 {} 进行初始化,也就是我们所说的初始化列表。 而对于类对象的初始化,要么需要通过拷贝构造、要么就需要使用 () 进行。 这些不同方法都针对各自对象,不能通用。
int main() { auto [x, y, z] = f(); std::cout << x << ", " << y << ", " << z << std::endl; return 0; }
2.3 类型推导
在传统 C 和 C++ 中,参数的类型都必须明确定义,这其实对我们快速进行编码没有任何帮助,尤其是当我们面对一大堆复杂的模板类型时,必须明确的指出变量的类型才能进行后续的编码,这不仅拖慢我们的开发效率,也让代码变得又臭又长
C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。这使得 C++ 也具有了和其他现代编程语言一样,某种意义上提供了无需操心变量类型的使用习惯
auto
auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着 register 被弃用(在 C++17 中作为保留关键字,以后使用,目前不具备实际意义),对 auto 的语义变更也就非常自然了
使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器。你应该在前面的小节里看到了传统 C++ 中冗长的迭代写法:
1 2 3 4
// 在 C++11 之前 // 由于 cbegin() 将返回 vector<int>::const_iterator // 所以 it 也应该是 vector<int>::const_iterator 类型 for(vector<int>::const_iterator it = vec.cbegin(); it != vec.cend(); ++it)
class MagicFoo { public: std::vector<int> vec; MagicFoo(std::initializer_list<int> list) { // 从 C++11 起, 使用 auto 关键字进行类型推导 for (auto it = list.begin(); it != list.end(); ++it) { vec.push_back(*it); } } }; int main() { MagicFoo magicFoo = {1, 2, 3, 4, 5}; std::cout << "magicFoo: "; for (auto it = magicFoo.vec.begin(); it != magicFoo.vec.end(); ++it) { std::cout << *it << ", "; } std::cout << std::endl; return 0; }
一些其他的常见用法:
auto i = 5; // i 被推导为 int
auto arr = new auto(10); // arr 被推导为 int *
从 C++ 20 起,auto 甚至能用于函数传参,考虑下面的例子:
1 2 3 4 5 6 7
int add(auto x, auto y) { return x+y; }
auto i = 5; // 被推导为 int auto j = 6; // 被推导为 int std::cout << add(i, j) << std::endl;
注意:auto 还不能用于推导数组类型:
1 2 3 4
auto auto_arr2[10] = {arr}; // 错误, 无法推导数组元素类型
2.6.auto.cpp:30:19: error: 'auto_arr2' declared as array of 'auto' auto auto_arr2[10] = {arr};
decltype
decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 typeof 很相似:decltype(表达式)
有时候,我们可能需要计算某个表达式的类型,例如:
1 2 3
auto x = 1; auto y = 2; decltype(x+y) z;
你已经在前面的例子中看到 decltype 用于推断类型的用法,下面这个例子就是判断上面的变量 x, y, z 是否是同一类型:
1 2 3 4 5 6
if (std::is_same<decltype(x), int>::value) std::cout << "type x == int" << std::endl; if (std::is_same<decltype(x), float>::value) std::cout << "type x == float" << std::endl; if (std::is_same<decltype(x), decltype(z)>::value) std::cout << "type z == type x" << std::endl;
其中,std::is_same<T, U> 用于判断 T 和 U 这两个类型是否相等。输出结果为:
type x == int
type z == type x
尾返回类型推导
你可能会思考,在介绍 auto 时,我们已经提过 auto 不能用于函数形参进行类型推导,那么 auto 能不能用于推导函数的返回类型呢?还是考虑一个加法函数的例子,在传统 C++ 中我们必须这么写:
1 2 3 4
template<typename R, typename T, typename U> R add(T x, U y) { return x+y; }
注意:typename 和 class 在模板参数列表中没有区别,在 typename 这个关键字出现之前,都是使用 class 来定义模板参数的。但在模板中定义有嵌套依赖类型的变量时,需要用 typename 消除歧义
在 C++11 中这个问题得到解决。虽然你可能马上会反应出来使用 decltype 推导 x+y 的类型,写出这样的代码:decltype(x + y) add( T x, U y)
但事实上这样的写法并不能通过编译。这是因为在编译器读到 decltype(x+y) 时,x 和 y 尚未被定义。为了解决这个问题,C++11 还引入了一个叫做尾返回类型(trailing return type),利用 auto 关键字将返回类型后置:
1 2 3 4
template<typename T, typename U> auto add2(T x, U y) -> decltype(x+y){ return x + y; }
令人欣慰的是从 C++14 开始是可以直接让普通函数具备返回值推导,因此下面的写法变得合法:
1 2 3 4
template<typename T, typename U> auto add3(T x, U y){ return x + y; }
可以检查一下类型推导是否正确:
1 2 3 4 5 6 7 8 9 10
// after c++11 auto w = add2<int, double>(1, 2.0); if (std::is_same<decltype(w), double>::value) { std::cout << "w is double: "; } std::cout << w << std::endl;
// after c++14 auto q = add3<double, int>(1.0, 2); std::cout << "q: " << q << std::endl;
decltype(auto)
decltype(auto) 是 C++14 开始提供的一个略微复杂的用法。要理解它你需要知道 C++ 中参数转发的概念,我们会在语言运行时强化一章中详细介绍,你可以到时再回来看这一小节的内容。
#include #include void foo(std::shared_ptr i) { (*i)++; } int main() { // auto pointer = new int(10); // illegal, no direct assignment // Constructed a std::shared_ptr auto pointer = std::make_shared(10); foo(pointer); std::cout << *pointer << std::endl; // 11 // The shared_ptr will be destructed before leaving the scope return 0; }
辅助数据由一个或多个辅助数据对象(ancillary data object)构成,每个对象以一个定义在头文件<sys/socket.h>中的cmsghdr结构开头
声明:
1 2 3 4 5
struct cmsghdr { socklen_t cmsg_len; /* length in bytes, including this structure */ int cmsg_level; /* originating protocol */ int cmsg_type; /* protocol-specific type */ };
System V消息队列(System V message queue,第六章),是在20世纪80年代早期加到System V内核中的。它们可用在同一主机上有亲缘关系或无亲缘关系的进程之间。(谈论Unix进程时,有亲缘关系(related)的说法意味着所论及的进程具有某个共同的祖先。说的更明白点,这些有亲缘关系的进程是从该祖先进程经过一次或多次fork派生来的。我们还得注意,从理论上来说,所有Unix进程与init进程都有亲缘关系,它是系统自举时启动所有初始化进程的祖先进程。然而从实践上说,进程亲缘关系开始于一个登录shell(称为一个会话)以及由该shell派生的所有进程
struct mq_attr { long mq_flags; /* message queue flag: 0, O_NONBLOCK */ long mq_maxmsg; /* max number of messages allowed on queue */ long mq_msgsize; /* max size of a message (in bytes) */ long mq_curmsgs; /* number of messages currently on queue */ };
System V IPC所存在的三种内核限制往往需要系统管理员对它们进行调整,因为它们的默认值通常不能满足现实应用的需要(3.8节)。Posix IPC所存在的三种内核限制则通常根本不需要调整
有关System V IPC对象的信息(当前大小,属主ID,最后修改时间等等)可使用三个XXXctl函数的IPC_STAT命令获取,也可执行ipcs命令获取。有关Posix IPC对象的信息则不存在标准的获取方式。如果这些对象是用文件系统中的文件实现的,而且我们知道从Posix IPC名字到路径名的映射关系,那么这些对象的信息可使用stat函数或ls命令获取。但是如果这些对象不是使用文件实现的,那么可能获取不了这样的信息
否则,handler就是用户定义的函数的地址,这个函数被称为信号处理程序,只要程序接收到一个类型为signum的信号,就会调用这个程序。通过把处理程序的地址传递到signal函数从而改变默认行为,这叫做设置信号处理程序(installing the handler)。调用信号处理程序被称为捕获信号。执行信号处理程序被称为处理信号
int sock; server.sin_family = AF_INET; server.sin_addr.s_addr = htonl(INADDR_ANY); server.sin_port = htons((short)8652); if (bind(sock, (struct sockaddr *)&server, sizeof(server)) == -1) perror("Failed to bind the socket to port !\n);`