不积小流,无以成江海

海纳百川,有容乃大

2018/9/19 17:46:55 这篇罗列概念很没水平,就别看了。

后记:这篇笔记限于罗列书中的概念,自己的理解并不多,组织章节也不好。后来又整理过一篇: 《再谈初始化》 ,两者不分先后,没有依赖关系。

参考《C++ Primer》和网上资源誊抄完正文后,自己总结如下:

  1. 定义一个变量/对象时,如果不进行显式地初始化,就是默认初始化(除了使用 extern 进行纯粹的声明的)。需要关注的是,默认初始化时编译器究竟赋予了变量/对象什么内容。

  2. 默认初始化变数太多,比如造成内置类型未定义行为,对于使用编译器合成的默认构造函数的类也可能产生不良影响,所以在定义变量时最好进行初始化!初始化的语法格式有好多种呢:

    • 根据是否使用等号分为拷贝初始化和直接初始化;
    • 根据是否使用了花括号,区分出列表初始化;
  3. 想在初始化时偷懒,不想写太多参数(vector 值初始化);但又操心默认初始不好使(new 动态分配时值初始化)。所以,又出现了值初始化。

阅读全文 »

动态存储(189~)

  • 堆上的数据不会自动清除,因此堆是保存数据结构的绝佳场所。

    可以把在堆上保存数据想象成在储物柜中寄存物品。

  • 当程序不断地申请存储器,又不释放那些不再需要的存储器,就会发生存储器泄漏

    创建数据不会发生泄漏,只有当程序失去了所有对数据的引用才会导致泄漏。

  • malloc()获取空间,memory allocation;调用free()释放存储器

    • malloc()接收一个参数:所需要的字节数。因此malloc()经常与sizeof运算符一起使用。
    • malloc()返回的是通用指针,即void*类型的指针。
  • strdup()函数可以把字符串复制到堆上。

    strdup()函数总是在堆上创建空间,而不是在栈上。所以千万记得要用free()函数释放空间。

  • valgrind工具,它用于Linux操作系统中。

    当程序想分配堆存储器时,valgrind将会拦截你对malloc()free()的调用,然后调用自己的malloc()free()

  • 垃圾收集(garbage collection),一些语言会跟踪你在堆上分配的数据,当你不再使用这些数据时,就会释放它们。C语言没有“垃圾收集”

虽然不必在程序结束前释放所有数据,操作系统会在程序结束时清除所有存储器。不过,你还是应该显式释放你创建的每样东西,这是一种好的习惯。

——很不懂作者为什么在章节快结束的时候,放上一句这么没营养的话。有机会去查一查原文,确认是不是译者的问题。个人认为以下表述更恰当:
虽然操作系统会在程序结束时清除所有存储器。不过,你还是应该显式释放你创建的每样东西,这是一种好的习惯。

高级函数(201~)

函数指针

此章节介绍“学习如何把函数作为参数传递,从而提高代码的智商”。

有些时候,我们希望“把代码打包传给函数”。“把代码打包”就是封装成函数,所以,问题就是:怎么把函数传给函数?

  • 在C语言中,函数名也是指针变量。

    当你创建了一个叫go_to_warp_speed(int speed)函数的同时也会创建了一个叫go_to_warp_speed的指针变量,变量中保存了函数的地址。只要把函数指针类型的参数传给find(),就能调用它指向的函数了。

    上述中go_to_warp_speed指针变量是一个常量。类似 int* a=4中的4, char* str="abc"中的"abc"

    扩展

    1
    2
    3
    4
    5
    6
    /* 此处声明时的*和 赋值操作时的*应不是同一个意思?!
    那么为什么初始化不写成int* a=&4?? —就是声明了一个指针,指向了常量存储区的一块地址 */
    int* a; //声明
    int b=4;
    *a=4; //改变指针指向存储器中的值,但单纯这么写是错的
    a=&b; //给指针赋值,改变指针指向的存储器
  • 函数名是指向函数的指针……两者并不完全相同,函数名是L-value,而指针变量是R-value,因此函数名不能像指针变量那样自加或自减。

  • 函数指针是C语言最强大的特性之一。

函数指针的语法

  • 没有函数类型,即:不能用function * pointer_func 声明函数指针。
  • 因为需要把函数的返回类型和接收参数类型告诉C编译器,所以创建函数指针如下:

    返回类型(* 指针变量)(参数类型),例如

    1
    2
    3
    int (*warp_fn)(int);  //创建函数指针
    wrap_fn = go_to_warp_speed; //函数指针赋值
    wrap_fn(4); //使用函数指针

    Q1:如果函数指针是指针,为什么调用时不需要再它们前面加*

    A1:可加可不加。wrap_fn(4)(*wrap_fn)(4)都可以。如果fp是函数指针,那么可以用fp(参数,……)调用函数。 也可以用(*fp)(参数,……),两种情况都能工作。

    Q2:可以用&取得函数的地址吗?

    A2:可以。find(sports_or_workout)find(&sports_or_workout)都可以。如果你有函数shoot(),那么shoot&shoot都指向了shoot()函数。

    Q3:那为什么不这么写?

    A3:即使省略*&,C编译器也能识别他们,(而且)这样代码更好读。

    延伸:由A3推测,是不是 常量4或者"abc"既表示本身的值,又代表其在常量存储区的地址?此处亦类似?

C标准库的排序函数

  • C标准库的排序函数会接收一个比较器函数(comparator function)指针,用来判断两条数据的大小。

  • qsort()函数看起来像这样:

    1
    2
    3
    4
    qsort(void* array,       //数组指针
    size_t length, //数组长度
    size_t item_size, //数组中每个元素的长度
    int (*comparator)(const void*, const void*));
  • qsort()函数在原数组上进行改动。

创建函数指针数组

  • 在数组中保存函数,就必须告诉编译器函数的具体特征:函数返回什么类型以及接收什么参数。

    1
    void (* replies[])(response) = {dump, second_chance, marriage};
  • 函数指针数组让代码易于管理,它们让代码变得更短、更易于扩展,从而可以伸缩。
    ——至于说降低程序可读性,那是因为你笨。

可变参数函数(variadic function)(228~)

C标准库中有一组宏(macro)可以帮助你建立自己的可变参数函数。
——TODO:kindle中描述这一组宏的图片看不清,抽空在网上找一下。

静态库和动态库

(省略。在接触Linux下C++开发之后,这方面知识不算陌生。)

系统调用(249~)

在大部分计算机上,系统调用就是操作系统内核中的函数,是程序用来与内核对话的函数。

system()

  • 简单易用

  • 安全问题

  • 无法应付复杂的应用场景

    操作系统必须解释你传给 system()的字符串,这可能引发错误,尤其当你动态创建命令字符串时。

exec()

  • exec() 函数通过运行其他程序来替换当前进程。

    • exec()函数的版本众多,但可以分为两组:列表函数和数组函数。
    • 如果exec()调用成功,当前程序就会停止运行。一旦程序运行了exec()以后的代码,就说明出了问题。
  • 每个进程都有一组环境变量。

失败黄金法则

  • 尽可能收拾残局

  • errno变量设为错误码

    • errno变量是定义在errno.h中的全局变量,和它定义在一起的还有很多标准错误码
    • 可以使用string.hstrerror()函数查询标准错误信息
  • 返回-1

    • 系统调用出错时通常会返回-1,但不是绝对的。
    • fileno():根据文件指针获取文件描述符。失败时不返回-1
    • exit()是唯一没有返回值而且不会失败的函数。
    • 记住:一定要检查系统调用的返回值

fork()

  • fork()会克隆当前进程

    • 新建副本将从同一行开始运行相同程序。
    • 原进程叫父进程,而新建副本叫子进程。
  • 进程需要以某种方式区分自己是父进程还是子进程

    fork()会返回一个整型值:为子进程返回0,为父进程返回一个正数。父进程将接收到子进程的进程标识符。

  • 与Unix和Mac不同,Windows天生不支持fork()

进程间通信(274~)

重定向

  • 三大默认数据流:标准输入、标准输出和标准错误;文件连接和网络连接也属于数据流。

  • 进程需要记录数据流的连向,比如标准输出连到了哪里。进程用文件描述符(就是个数字)表示数据流

  • 描述符表:描述符表的前三项万年不变:0号标准输入,1号标准输出,2号标准错误

    • 标准输入/输出/错误在描述符表中的位置是固定的,但它们指向的数据流可以改变。
    • 每打开一个文件,操作系统都会在描述符表中新注册一项
  • dup2()复制数据流

  • exit()是系统调用!

    exit()系统调用是结束程序的最快方式

  • waitpid()函数会等子进程结束以后才返回

    • 为了得到子进程的退出状态,可以把pid_status的值传给WEXITSTATUS()

管道

  • 在命令行用管道连接两条命令时,实际把它们当成了父子进程来连接。child | parent,详见shell中管道的原理

  • pipe()函数建立管道

    pipe()函数创建了管道,并返回了两个描述符:fd[1]用来向管道写数据,fd[0]用来从管道读数据,

信号

  • 当信号到来时,进程必须停止手中一切工作去处理信号。进程会查看信号映射表,表中每个信号都对应一个信号处理器函数。

捕捉信号,然后运行自己的代码

  • sigaction是一个函数包装器,是一个结构体。
  • 创建sigaction以后,需要用sigaction()函数来让操作系统知道它的存在
  • 有一个例外,代码捕捉不到SIGKILL信号,也没法忽略它
  • 在自定义的信号处理函数中使用raise(),这样程序就能在接收到低级别的信号时引发更高级别的信号

定时器

  • 不要同时使用alarm()sleep()。两者都使用了间隔计时器,因此同时使用这两个函数会发生冲突。

  • 一个进程只有一个定时器。因此每次调用alarm()函数都会重置定时器

    定时器由操作系统的内核管理,如果一个进程有很多定时器,内核就会变得很慢,因此操作系统需要限制进程能使用的定时器个数。

  • 信号可以让程序从容结束,而间隔定时器可以帮助处理一些超时任务。

网络编程(323~)

C程序用数据流读写字节。如果想要写一个与网络通信的程序,就需要一种新数据流——套接字。

  • 在使用套接字与客户端程序通信前,服务器需要经历四个阶段:

    1. 绑定(Bind)

      • 为了绑定它(端口),你需要两样东西:套接字描述符(还记得“文件描述符”吗?)和套接字名。
    2. 监听(Listen)

    3. 接受(Accept)

    4. 开始(Begin)

  • 之前见过的数据流都可以用fprintf()fscanf()与它们通信。但套接字是双向的:

    • 如果想向套接字输出数据,就要用send()函数;
    • recv()函数;
  • 绑定端口有延时:当你在某个端口绑定了套接字,在接下来的30秒内,操作系统不允许任何程序再绑定它,包括上一次绑定这个端口的程序。

  • recv()读取数据

    • 字符串不以\0结尾。
    • 当用户在telnet输入文本时,字符串以\r\n结尾。
    • recv()将返回字符个数,如果发生错误就返回-1,如果客户端关闭了连接,就返回0
    • recv()调用不一定能一次接收到所有字符。
    • revc()用起来十分繁琐,最好把它封装在某个函数中
  • getaddrinfo()获取域名的地址

    • getaddrinfo()会在堆上创建一种叫名字资源的新数据结构
    • 因为名字资源在堆上创建,所以要用一个叫freeaddrinfo()的函数清除它

多线程(342~)

你可以使用很多线程库,这里我们将使用最流行的一种:POSIX线程库,也叫pthread。

  • 线程函数的返回类型必须是void*

  • 可以用pthread_create()创建并运行线程。

  • 如果程序运行完这段代码就结束了,线程也会随之灭亡,因此必须等待线程结束:

    • pthread_join()会接收线程函数的返回值,并把它保存在一个void指针变量中
  • 通常当两个线程读写相同变量时,代码就是非线程安全的。

  • PTHREAD_MUTEX_INITIALIZER实际上是一个宏,当编译器看到它,就会插入创建互斥锁的代码。

  • 如果你希望把某个整型值传给线程,并让它返回某个整型值,一种方法是:

    • long,因为它的大小和void指针相同,可以把它保存在void指针变量中

问:怎样设计高效的多线程程序? 答:减少线程需要访问的共享数据的数量。

写在最后

另一种理解思路:

  • C语言允许你创建只能在函数局部作用域访问的全局变量:

    • 函数内:static关键字会把变量保存在存储器中的全局量区。
  • static关键字用来控制变量或函数的作用域

    • 函数外使用static关键字,表示“只有这个.c文件中的代码能使用这个变量(或函数)”

关于“自动化测试”,你又了解多少?

之前读书时做的笔记全部誊抄、整理完毕,上篇中的知识点基本都已熟悉掌握,下篇的内容因为学校时练习不多,实际做项目经验也很少,所以只处于了解、知道的层次,勤复习,多练习。笔记内容包含编程过程中的“为什么这样”,也包括一些具体的语法细节、函数使用(实际上后者是不应该杂糅到一起的,但是基础太差)。在以后的复习、练习过程中,争取将语法细节、函数使用这些具体化的内容一点点去掉。

C++ 获取系统时间

有的帖子真心写的很好,你都想帮作者打广告。有的烂到即使你从里面学到了知识,你也不想再见到它,你都不想在自己的笔记里放它的链接,不然说不准哪天又点进去浪费自己的时间。

说一下思路:

  1. 使用开发语言自身的标准库。对于 C++ 来说,使用 C 的或者 C++ 的
  2. 使用第三方库,如果有的话
  3. 使用系统相关 API。缺点在于不能跨平台
阅读全文 »

使用 cpp 中常用容器时,的注意事项。

不要保存 end 返回的迭代器

当我们添加或删除 vectorstring 的元素后,或在 deque 中首元素之外任何位置添加或删除元素后,原来 end 返回的迭代器总是会失效。因此,添加或删除元素的循环程序必须反复调用 end,而不能在循环之前保存 end 返回的迭代器,一直当做容器末尾使用。

std::map 遍历删除元素

对容器进行增、删元素操作,可能会使迭代器失效。关于这一点,复习 《C++ Primer》 P315

正确的写法:(只需要记住正确的写法,其他的即便不是错的也是不规范的)

C++11 标准下,使用 erase() 返回值更新迭代器:返回值 Iterator following the last removed element.

1
2
3
4
5
6
7
// erase all odd numbers from c
for(auto it = c.begin(); it != c.end(); ) {
if(it->first % 2 != 0)
it = c.erase(it);
else
++it;
}

C++11 之前,erase() 成员没有返回值。怎么遍历删除自行 Google 吧

阅读全文 »

在《Effective C++》条款 27 强调尽量少做转型动作,参考此条款中的内容整理笔记。

在 C++ 中转型是一个你会想带着极大尊重去亲近的一个特性。

我们先回顾转型的语法,然后回过头再看怎么赋予类型转型的能力。

隐式转型

我们经常接触的隐式转换发生于内置类型中的数值类型之间。但往往因为过于自然,反而忽略了转型动作的存在。

阅读全文 »

由生产者-消费者模式展开,学习其编程的最佳实践,却发现自己不知“信号量”的概念,忘了PV操作,困惑于“使用数组做循环队列时,判断满、空的条件怎么写”,为什么不直接使用 STL 的容器做队列?进出队列是不是严格需要锁?获取队列大小时是不是严格需要锁?

进程和线程管理

参考来源:C 语言中文网

进程控制块(PCB)

为了使参与并发执行的程序(含数据)能独立地运行,必须为之配置一个专门的数据结构,称为进程控制块(Process Control Block, PCB)。系统利用PCB来描述进程的基本情况和运行状态,进而控制和管理进程。相应地,由程序段、相关数据段和PCB三部分构成了进程映像(进程实体)。所谓创建进程,实质上是创建进程映像中的PCB;而撤销进程,实质上是撤销进程的PCB。值得注意的是,进程映像是静态的,进程则是动态的。

阅读全文 »

在学习 C++ 的时候经常见到“资源获取即初始化”这句话。一直困惑这句话要表达的意思,是强调“资源获取”还是强调“初始化”,字面意思:获得系统资源这个行为就是“初始化”。不理解到底是指什么。

维基百科怎么说?

我们看 RAII-Wikipedia 中的叙述:

RAII全称为Resource Acquisition Is Initialization,它是在一些面向对象语言中的一种惯用法。XX 在设计C++异常时,为解决资源管理时的异常安全性而使用了该用法。

RAII要求,资源的有效期与持有资源的对象的生命期严格绑定,即由对象的构造函数完成资源的分配(获取),同时由析构函数完成资源的释放。在这种要求下,只要对象能正确地析构,就不会出现资源泄露问题。

阅读全文 »

The Biggest Changes in C++11 (and Why You Should Care),译文:C++11 的重大改变

事实上,核心 C++11 已经有了很大的改变。C++11 标准库同样增加了新的内容。捎带一句,C++11, also formerly known as C++0x …

Core

事实上,核心 C++11 已经有了很大的改变。现在它支持 lambda 表达式,自动类型推断,统一的初始化语法,委托构造函数,已删除和默认函数声明,nullptr,以及最重要的,右值引用——一种预言将会改变创造和处理对象方法的技术。

Lambda 表达式

lambda 的函数调用运算符为 const-by-value,但对 mutable 关键字的使用可将其取消。

自动类型推断和 decltype

赋予 auto 关键词新生: 数据存储类型

auto 自动推导类型,轻度用户(不涉及模板)谨慎使用,在书写便利和代码易读两者之间寻求,滥用降低代码可读性。

注意使用的限制

1
2
3
4
5
6
size_t length = 10;
int diff = 0 - length;
auto diff2 = 0 - length;
// diff != diff2
cout << (0 - length) << endl;
// NOTE 并未像我们预期的那这样子输出 -10

统一初始化语法

列表初始化:初始化操作的多种变体是令人感觉困扰的重要原因之一。C++11 使用统一的大括号标记清除了这种混乱。

POD (程序设计) - wikipedia

看了以上几篇帖子之后,知道了几个名词:Aggregate 和 POD,trivial 和 standard-layout。但详细的概念很是混乱,其中一条条的“是与不是”、“有与没有”难道是让人用来背诵记忆的吗?实用主义不要求学究钻研,编码过程用到了再回来复习。目前,水过不学了。其实真要较真的话,应该直接去找 C++ 的新标准,网上的终究是个参考,会有出入。

删除和默认函数

  • 多看书,多写代码

nullptr

  • 在此之前是什么样子?会有什么问题?深度探索一下。

  • c++11 中的 nullptr 详解

  • NULL 和 0 - 蓝色的回答

    • 不推荐使用宏,甚至不承认宏。包括 NULL,C++标准是没有承认 NULL 是 null pointer constant 的。
    • 赋予 null pointer,应该是使用 0,而非 NULL

委托构造函数

右值引用

  • 区分“左值”和“右值”。引用自 右值引用 - wikipedia

    在C++11提出右值引用之前,C++03及更早的C++标准中,表达式的“值分类”(value categories)属性为左值或右值。左值是对应(refer to)内存中有确定存储地址的对象的表达式的值,而右值是所有不是左值的表达式的值。因而,右值可以是字面量、临时对象等表达式。能否被赋值不是区分C++左值与右值的依据。C++的const左值是不可赋值的;而作为临时对象的右值可能允许被赋值。左值与右值的根本区别在于是否允许取地址&运算符获得对应的内存地址

  • 移动语义?学习 C++ Prime

    • 移动构造函数、移动赋值运算符

    C++11 标准库大量使用了移动语义。许多算法和容器也为移动语义做了优化。

  • 【译】详解 C++ 右值引用,阅读笔记如下

    1. 概述中 # &foobar() 是取 foobar() 函数执行结果的地址,而不是函数指针,&foobar 才是函数指针。

    2. 右值引用,总算渐渐明朗了。但前提是分清左值/右值。

      const 常量是左值,还是右值呢?——左值,因为 const int max = 100; 中 max 是可以使用 & 取地址的。

    3. move语义# 中“当赋值操作符的右边是右值的时候,我们希望赋值操作符被定义成下面这样:”,醍醐灌顶啊

      此处的右值,在后续代码中可能会再次被用来给变量赋值吗?

      “move语义:当一个变量(a)作为拷贝构造函数或者赋值的来源时,这个变量要么就是以后都不会再使用,要么就是(使用时)作为赋值操作的目标(a = b)。”

      也就是,这个变量不能再用了。

      ——参考 右值引用是右值吗?#

      “move语义的重点在于将其应用于那些不重要的东西上面,那些move之后会马上销毁而不会被再次用到的东西上面。”

    4. 强制move语义# 中:

      C++11中的swap函数是这样的:…

      swap函数不能只做 a = std::move(b); 吗?

      值得注意的是对那些没有实现move语义的类型来说(没有针对右值引用重载拷贝构造函数和赋值操作符),新的swap仍然和旧的一样。

      可能只是为了兼容。参考 Debian8Light 代码验证。

    5. 右值引用是右值吗# 中读不懂:

      理论上来说goo()所引用的对象也可能在X x = goo();后被访问的到。但是回想一下,这种行为不正是我们想要的吗?我们也想随心所欲的在左值上面使用move语义。

    6. move语义与编译器优化# 暂时读不懂

    7. 完美转发相关内容暂时未看,涉及范型编程之泛型函数

  • C++11 标准新特性: 右值引用与转移语义

  • 一文读懂 C++ 右值引用和 std::move

  • 左值引用、右值引用

Library

2003年,C++ 以库技术报告 1(TR1)的形式经历了一次大型重构。TR1 包含了新的容器类(unordered_setunordered_mapunordered_multisetunordered_multimap)和许多新的库,例如正则表达式,元组,函数对象包装器。随着 C++11 的颁布,TR1 连同新的库一起正式集成到 C++ 标准中。下面是 C++11 标准库的特性:

线程库

毫无疑问,从程序员角度看,C++11 最重要的改进就是并发。

新的智能指针类

C++98 只定义了一个智能指针类 auto_ptr,而这个类现在已经被废弃了。

C++11 包含了新的智能指针类:shared_ptrweak_ptr 和最近新加的 unique_ptr

weak_ptr 是多线程下,判断某一(在多个线程中共享的)对象是否 alive 的唯一方法。

新的算法

THE END

延伸阅读:

笔记针对以下问题:

  1. 在c语言中,sizeof运算符和strlen()函数有什么区别的?
  2. 如果/字符串/字符指针/用来存储汉字,对于这两者的计算结果有影响吗?
  3. 在C++中,sizeof运算符和strlen()函数的定位?
  4. 在C++中,string的lengt()size()
  5. 在C++中,string用来存储汉字时,……
阅读全文 »

在 C++ 中怎么去除字符串首尾的空格?怎么去掉字符串中所有的空格?

网上可以查到很多,有用 C 写的,有用纯 C++ 写的,混合的更是大有人在。功能都能实现,但哪个是最佳实践的?有以下几个标准 or 问题?

  • 代码足够简洁
  • 怎么保证效率?
  • 通用性:跨平台
  • 要不要考虑 C 调用问题?
阅读全文 »
0%