C++ Primer读书笔记

第2章
  • 有符号类型与无符号类型不能混用
  • 定义为函数外的变量都将被默认初始化(置零),函数内的默认初始化则允许有未初始化变量存在
  • 静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
  • 引用必须在定义处初始化,后续无法再将引用绑定到其它对象上,引用即别名,不是对象
  • 指针是一个对象,使用nullptr或0来初始化未知指针,void*是一种特殊的指针类型,可用于存放任意对象的地址,但无法访问地址上的对象(类型未知)
  • 类型修饰符*&等只用于修饰声明符(变量名),而不是修饰数据类型
  • const对象一旦创建后其值不能再改变,所以必须在定义处初始化,const对象仅在文件内有效,在定义或声明前加上extern可使其被多文件共享
  • 一般情况下引用类型必须与其引用对象类型一致,但编译器允许初始化常量引用时使用类型转换,允许常量引用绑定到非常量的对象
  • Const int *p为指向常量的指针,指针值可以改变,而int *const p为常量指针,指针值不能变化(从右向左读可以明确数据类型),顶层const即常量指针,底层const即对象为常量,constexpr限定为顶层const
  • 使用auto来定义常量时,顶层const会被忽略,底层const会被保留,如果希望实现顶层const,则使用const auto来定义
  • decltype()返回括号内的数据类型,用于定义处声明数据类型
  • 预处理器可以确保头文件多次包含仍能安全工作,预处理功能主要采用头文件保护符实现,包括#include #ifdef #ifndef,如果def判断为真,则执行到#endif处,这样就可以保证#include时只把相同的头文件包含进来一次
  • 最大的int是0x7fffffff,最小的int是0x80000000

 
第3章
  • string要include<string>,其定义在namespace std中
  • string对象使用+号或append函数都可以进行追加内容
  • string的输入有两种方法,cin>>str遇到空格则停止,getline(cin,str)遇到换行符停止,而char*只能用cin>>a来直接输入,或调用cin.getline(a,size)来读入整行,size要包含'\0'元素,所以读入的实际元素数目为size-1
  • Char* 可以直接用cout输出整个字符串数组,注意数组中最后一个元素需要是'\0',string也可以直接用cout输出
  • String.size()函数返回无符号整型数,因此不要再与int型混用,定义时用string::size_type,用+号给string赋值时需确保+号两侧至少有一个是string类型
  • vector是一个模板,其生成的类型会包含<>中的类
  • vector可以使用=号进行复制初始化,使用ivec(10,"hi")进行初始化,使用ivec{"","",""}进行列表初始化,但使用列表初始化时若元素类型不对,则会将{}转为()
  • 传值范围for循环中不能对vector进行增删,范围for循环中若使用引用的类型,则可以对容器或数组中的数据做修改,要使用范围for循环处理多维数组时,不管是否要修改元素,除了最内层的循环外,其它所有循环的控制变量都得是引用类型,不然会试图遍历一个int*而报错,用auto&定义或int (*p)[10] 来定义
  • String::size_type为string.size()返回类型,vector<int>::size_type为ivec.size()返回类型,注意要加上<int>,也可以用decltype(ivec.size())来确定类型
  • 迭代器s.begin()指向头元素,s.end()指向尾元素的下一位置,因为s.end()并不指向某元素,故不能对其进行++或解引用,可以进行--操作
  • String::iterator为迭代器类型,const_iterator为常量指针类型,底层const,cbegin和cend返回const_iterator迭代器
  • 使用char a[]="abc"初始化的字符串数组会自动在后面加一个空字符(值为0),因此不要在定义该数组时按照初始化长度给定数组长度,数组不允许整个拷贝
  • C风格的字符串数组以'\0'结尾,strlen、strcmp、strcat、strcopy都依赖于此特性,因此这些函数只能用于C风格字符串数组
  • 理解数组指针或引用定义语句时,从内向外读,int (*p)[10]=&arr即表示p为指针,指向长度为10的数组,数组类型为int
  • begin(arr)和end(arr)返回数组的指针,可以根据end()-begin()来获得数组的长度,数组下标可以为负值,而标准库容器类型限定下标必须是无符号类型
  • 结合使用auto和*来避免数组指针类型的混乱:arr+2指向第三行首元素
int arr[3][4];
for(auto p=arr;p!=arr+3;++p)//使用begin(arr)和end(arr)更简洁
for(auto q=*p;q!=*p+4;++q)//对应begin(*p)、end(*p)

 

  • 多维数组arr转化为指针后arr相当于带有两个&符号,因此q=*p为一维数组的头指针,*q相当于解引用获取对象

 

第4章
  • sizeof(typt)或sizeof expr可以求得类型或表达式结果的占用内存大小(字节为单位),对空类型求sizeof会得1,因为声明该类型实例时必须占一定的空间,VS决定了这个空间是1字节
  • 尽量不要混用有符号数和无符号数,有符号数与无符号数进行运算时,谁占用的内存大,另一个数就得转换到该类型,一样大时转为无符号数
  • 强制类型转换包括四种:static_cast、dynamic_cast、const_cast、reinterpret_cast,使用方法为cast-name<type> (expr),其中type为要转换目标类型
  • 只要不包含底层const的类型转换都可以使用static_cast,const_cast为去掉const性质,reinterpret是不安全的强行解释(为通过编译)
  • 通常情况下static_cast只能用于其他合法的类型转化,显式写出以明确,又可以用于将左值转换为一个右值引用
  • Dynamic_cast实际上只接受基于类对象的指针或引用的类型转换,可以实现子类到父类的转换(需要是公有派生类)或父类到子类的转换(需要是公有继承),父类必须有虚函数,传入指针时若失败返回空指针,传入引用时若失败抛出bad_cast异常
  • 只有&& 、||、 条件运算符和逗号四种运算符明确规定了求值顺序是从左到右,其余的由编译器决定(不定)
  • 前置++运算符或--运算符计算得到一个左值,即可以继续对其赋值,而后置运算符计算得到一个右值,只能作为计算结果,也因此前置版本计算速度更快,省去了保留旧值的过程,求值结果值不同,求值结果类型不同,运算时间不同

 

第5章 语句
  • switch会将括号内的表达式转化为整型后与case比较,case标签必须是整型常量表达式
  • throw抛出异常,try块用来包含可能产生异常的单元,异常被抛出后会层层寻找合适的catch处理块,如果没有找到则转到terminate函数非正常退出程序
  • 若一个函数可能抛出异常,则应在函数声明及定义处加上void func() throw (exception),此处exception也可以是double/int/等等,说明不仅可以抛出异常,也可以抛出其它信息,而在函数内部,抛出动作不能用throw new exception("info"),否则异常就位于当前函数内存中了(没来得及传递给调用函数方程序就会被迫停止),而应该采用throw exception("info");然后在函数调用处采用try catch进行异常处理。


第6章 函数
  • 局部静态对象存在于从定义到程序终止的整个过程中,局部静态对象若没有显式地初始化,则会进行默认的值初始化(置零)
  • 让函数返回多个数据的第一种方法:形参中增加一个引用形参,把数据赋值给该引用指向的对象;第二种方法:返回initializer_list或vector;第三种方法:返回数组指针,int *func
  • using arrT=int[10];arrT* func(param),这样func就返回了一个二维数组指针,其中维度要指定,也可以使用int (*func(param))[10]或auto func(param) ->int(*)[10],同样返回数组指针,要注意不能返回在函数内部定义的数组 
  • 不能用底层const对象初始化一个非常量对象或引用,回顾通用的初始化规则可以推导实参传递给形参是否合法
  • 形参为引用或指针时,只能传入类型一致的引用或指针,存在非常量限制以及类型转换限制,尽量使用常量引用,一是可以明确函数的修改权,而是增加函数可以传入的实参类型范围
  • 顶层const无论是在形参中还是在实参中,都会被忽略,因为实参是通过拷贝传入的,为一临时量,无必要定为常量
  • 形参为数组指针时,可以写上期望的数组长度,但传递实参时并不检查长度是否一致,即便指向单个元素的指针也可以作为数组指针传入函数
  • 传递多维数组指针:print(int (*matrix)[10],int rowsize){}或print(int matrix[][10],int rowsize){},第一个维度同样被忽略了
  • Initializer_list与vector类似,是一种模板类型,但其中的元素都是常量值,调用时只能用{常量值…}
  • 如果函数返回内置类型,则只能返回一个值或花括号包起来的一个值,如果函数返回的是类类型,由类本身定义初始值如何使用(构造一个类的对象返回)
  • 一般情况下数组名都会自动转换为指向数组首元素的指针,但在sizeof()中为取数组整体内存长度(要严格传入数组名,若传入指针则为4),在&符号作用时,为取数组地址(将数组看做一个单元,&后+1直接到达数组尾元素的下一地址),在decltype()中,得到数组类型
  • 函数重载区分底层const和非常量,但不区分顶层const和非常量
  • 函数形参可以有默认实参值,但若一个形参有了默认值,其后的所有形参都必须有默认值,局部变量不能作为默认实参,全局变量(可以修改)或常量可以
  • 当使用一个函数的名字时,在需要的情况下它会自动转化为一个指针,相当于&funcname,不带括号
  • 声明函数返回类型为函数指针,先using PF=int(*)(int*,int),PF func(string),或者auto func(string) ->int(*)(int*,int);


第7章 类
  • 定义在类内部的函数是隐式的inline函数(inline定义的内联函数,函数代码被放入符号表中,在使用时进行替换(像宏一样展开),效率很高。)
  • struct和class唯一的不同是默认的成员访问权限,struct是public的,而class是private的
  • 类内成员不会自动初始化置零,默认构造器什么都不做
  • 编译器自动合成了类的拷贝、赋值和析构操作,如果类内都使用内置类型以及vector、string,那么这些操作可以正常运行
  • 在类内声明一个外部函数是friend的,则该函数可以访问此类的非公有成员
  • 类内的const成员函数中无法修改普通成员数据,可以修改mutable成员数据
  • 合成默认构造器用=default;来生成,其它构造器用类名(参数…):数据1(参数1),数据2(参数2){}的形式定义
  • 编译器只会自动地执行一步类型转换,例如const char*转换到string,string再转到别的类型就不会再自动进行了
  • 类的构造器若只接受一个实参,则在需要此类对象的地方,可以只传递该实参,将会自动执行类型转换生成一个该类的对象,称为隐式转换,发生隐式转换的另一情况是执行拷贝形式的初始化(使用=号)
  • 要抑制隐式转换,可以将构造函数声明为explicit的,且该构造函数必须在类内部定义
  • 类成员函数名后加const表明它们不会修改类对象,如果在const函数中修改了数据成员或调用了非const,编译器将指出错误
  • C++中不对类本身进行访问修饰(没有包概念)

 
第8章 IO库
  • 为支持宽字符版本的输入输出,istream或ostream都有另外的wistream wostream版本,通过继承机制,可以用同样的形式使用它们
  • IO流对象无拷贝操作,所以不能将形参或返回类型设为流类型,只能用其引用,读写IO对象会改变其状态,故不可以为const的
  • 流状态iostate有四种:badbit、goodbit、failbit和eofbit,分别对应错误、成功、失败和结束,good和fail为相反变量
  • 关联输入和输出流时,在试图从输入流读取数据都会先刷新关联的输出流
  • 当一个fstream对象被销毁时,close会自动被调用
  • 文件流模式有in out app ate trunc binary
  • iostringstream可以看作是保存string的一个流缓存

 
第9章 顺序容器
  • Vector、string将元素保存在连续的内存空间中,可以看作长度可变的数组,list、forward_list为双向和单向链表(只能在表头插入),deque是双端队列,相比vector只是加了个头部插入元素的快速操作
  • 当使用一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须匹配
  • 为使用array类型,必须同时制定array中元素类型与大小,如array<int,10>
  • assign函数用参数所指定的元素替换左边容器中的所有元素
  • Swap(ca,c2)会交换容器c1和c2,但并不交换元素,只是把容器指针换了,但swap两个array会真正交换两个array的元素值
  • 向vector、string或deque插入元素都会使指向容器的迭代器、引用和指针失效
  • insert函数在指定位置前插入元素,返回插入的最前位置
  • front、back和at函数返回的是引用类型,因此可以直接用c.front()=..来修改元素值
  • 容器的下标[2]与数组一样不进行越界检查,但使用at函数则会检查
  • 单向链表forward_list的添加或删除元素操作需要用到指定元素的前驱,但单向链表并无简单的方法来获取一个元素的前驱,因此其添加或删除元素是通过改变指定元素之后的元素来完成的,因此传递给函数insert_after、emplace_after和erase_after的参数是指定元素的前一个地址,before_begin函数返回首前迭代器
  • 添加或删除元素后end迭代器总会失效,所以不要保存它,循环中每次要更新c.end()
  • String.substr(2,5),返回一个子字符串(下标2-5)
  • 适配器有stack、queue和priority_queue,其将容器封装为栈、队列、优先级队列
  • 初始化顺序容器时可用另一个顺序容器的迭代器,范围类似begin()-end(),不包括end
  • Find()函数接受三个参数,begin(),end()和value,查找到即返回指向该元素的迭代器
 


第10章 泛型算法
  • 算法实际上是基于迭代器实现的,因此算法不会改变所操作的容器的大小,插入迭代器才可以执行插入操作
  • 接受三个迭代器参数的算法都默认假设第二个序列比第一个序列长
  • Back_inserter返回插入迭代器,通过迭代器赋值时会调用push_back插入元素,将其作为算法的实参也会进行插入操作
  • 谓词是一个可以调用的表达式,定义形式与普通函数一样,可以作为sort函数的第三个参数,用于对比元素大小,若<则返回true,谓词都为返回bool
  • lambda表达式相当于内联函数,可以作为对象传递,其形式为[capture list](parameter list)->return type{ …  },其中参数列表和返回类型可以忽略,但只有当函数体只有一句return语句时返回类型忽略才可以被正常编译
  • For_each函数对迭代器范围内的每一个元素都调用第三个参数(可以是lambda表达式也可以是一元谓词),在[]中写入=或&可以隐式捕获值或引用
  • transform接受三个迭代器和一个可调用对象,对范围内每个元素调用可调用对象,将值写到目的位置
  • Bind()用于绑定参数,相当于一个函数适配器,auto newCallable=bind(callable,arg_list);其中arg_list中包含_1这样的占位符,表明newCallable的参数_1放到callable的对应参数列表位置中,使用bind需要using namespace std::placeholders;
  • bind总是会拷贝参数,因此不能传递引用类型参数,但又可以调用ref(os)来返回一个可以被拷贝的引用
  • Back_inserter或front_inserter插入迭代器总是在容器的当前尾或头部插入元素,而inserter(c,c.begin())则会保持插入迭代器始终指向同一个元素
  • 反向迭代器传入sort(vec.rbegin(),vec.rend())可以将vec进行递减序排列
  • [line.crbegin(),rcomma)]和[rcomma.base(),line.cend()]指向同一区间元素
  • 对于list和forward_list,要优先使用其独有的成员函数sort、merge、remove、reverse和unique
 

第11章
  • 关联容器分八种,set和map为其基础类型,multi表示允许多关键字,unordered表示元素无序保存
  • map可以直接以关键字为下标[]返回关键字对应内容,pair.first表示关键字,pair.second表示内容
  • 若容器的元素类型没有默认的<比较操作,则在定义容器时需要将比较操作函数的指针传入,decltype(compfunc)*,decltype解析出的是函数类型,因此要再加上*,完整的定义为 multiset<Sales_data,decltype(compfunc)*> bookstore(compfunc);
  • 关联容器有key_type、mapped_type、value_type,对其迭代器解引用得到value_type,set、map的key_type是const的,因此不能通过解引用来修改
  • 使用迭代器遍历有序关联容器时,迭代器按关键字的升序遍历元素
  • 对map容器直接取下标后(得到mapped_type)赋值,可以直接将新元素插入容器中,因此只能对非const的map进行下标操作,而若用at成员函数,返回关键字匹配的元素,否则抛出异常
  • 在map中插入新元素,但新元素的关键字已存在的情况下,map会将原来的元素删除,存入最新的pair
  • 无序容器采用哈希函数+管理桶来为元素定址,哈希函数将关键字映射到桶,若桶中有多个元素,再进行顺序搜索来查找我们想要的元素
  • 若关键字为自定义类型,则需要为无序容器提供哈希函数hasher,返回size_t类型的值以供定址

 

第12章 动态内存
  • Shared_ptr保存指向对象的引用计数,当计数值为0,则释放对象占用内存
  • 使用动态内存的三个原因:1程序不知道自己需要使用多少对象2程序不知道所需对象的准确类型3程序需要在多个对象间共享数据
  • 使用new动态分配内存,返回一个指向该内存的指针,new int进行默认初始化,而new int()使用值初始化
  • 使用内置指针动态分配内存的三个普遍问题:1忘了delete,导致内存泄露2使用已经释放掉内存的对象3同一块内存释放两次
  • 结合使用shared_ptr和new,必须用直接初始化形式shared_ptr<int> a(new int);,不能用=号
  • 由于不能隐式转换,要改变智能指针指向另外的new出对象时不能用=号,要用reset()成员函数
  • 如果使用智能指针管理的资源不是new分配的内存,需要在定义时传递给它一个删除器
  • Unique_ptr指针独拥指向对象,因此不支持普通的赋值或拷贝
  • 定义动态数组返回的是元素类型的指针,因此不能使用数组用的begin、end和范围for循环语句,动态数据的delete []p要有[]
  • Unique_ptr的release函数不会释放内存,但数组版本的unique<int[]>会释放内存(自动调用delete[])
  • allocator是一个模板,帮助我们将内存分配与对象构造初始化分离开,单纯进行内存分配
  • Allocator.allocate(size_t)进行内存分配,destroy(*p)进行元素析构,deallocate进行内存释放

 

第13章 拷贝控制
  • 类的拷贝初始化由拷贝构造函数或移动构造函数来完成,用到=号就会调用拷贝初始化,传递非引用类型参数或返回非引用数据时,都会调用类的拷贝初始化来复制出一个对象
  • 合成拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中,数组类型成员会逐元素进行拷贝,合成赋值运算符与合成拷贝构造函数一致
  • 赋值运算符=一般返回一个指向其左侧运算对象的引用
  • 隐式销毁一个内置指针类型的成员不会delete它所指向的对象,而智能指针是类类型,其对象会被销毁
  • 类似于构造函数中成员是在函数体之前进行初始化的,析构函数中函数体自身并不直接销毁成员,而是在析构函数体之后隐含的析构阶段中销毁
  • 拷贝构造函数、拷贝赋值运算符和析构函数通常需要一起自定义或都不自定义
  • 构造函数、析构函数或运算符使用=default声明其采用编译器自动合成版本,使用=delete声明其为删除的函数,不可调用(析构函数不能是=delete)
  • 如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或是类有一个const的或引用成员,则类的合成拷贝赋值运算符被定义为删除的
  • 拷贝构造函数除了该类类型引用参数外,如有其它参数都需要赋予默认值
  • 如果类的某个成员的析构函数是删除的或不可访问的,或是类有一个引用成员,它没有类内初始化器,或是类有一个const成员,它没有类内初始化器且其类型未显式定义默认构造函数,则该类的默认构造函数被定义为删除的
  •  自定义赋值运算符时需要记住,如果将一个对象赋予它自身,赋值运算符必须能正确工作,大多数赋值运算符组合了析构函数和拷贝构造函数的工作
  • &&为右值引用,必须绑定到右值,只能绑定到一个将要销毁的对象
  • 可以将一个const的左值引用绑定到一个右值上,其它情况下左值引用和右值引用严格区分
  • 调用std::move()函数可以将处理一个左值返回一个右值,调用后传递参数的状态不再可用,只能进行赋值或销毁操作
  • 只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符
  • 定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则这些成员默认地被定义为删除的
  • 析构函数不会自动释放指针成员指向的对象
  • 移动迭代器在解引用时会得到一个右值引用

 

第14章 重载运算与类型转换
  • 当一个重载的运算符是成员函数时,this指针绑定到左侧运算对象
  • 输入运算符应该负责从输入异常或错误中恢复对象状态
  • 如果类同时定义了算术运算符和相关的复合赋值运算符,通常应该使用复合赋值来实现算术运算符
  • 赋值运算符必须定义成类的成员函数,复合赋值运算符通常也应该这样
  • Function<int(int,int)>能将函数、lambda表达式、函数对象都适配到同一个容器模板里
  • 类型转换有用但不一定安全时,应将oprator int(){return …}定义为explicit的,这样只有用static_cast<int>(a)时才能将a进行类型转换
  • 在if/while/for语句或!/||/&&/?:符号作用时,类型转换为bool总是能隐式执行

 

第15章 面向对象程序设计
  • 使用class A:public B来声明A继承自B,当使用public修饰时可以把A对象当成B的对象来使用,基类中public的到了派生类中一样时public的,此外还可以将派生类绑定到基类的引用或指针上
  • 基类中使用virtual来声明虚函数,每个派生类必须在自己类中再声明该虚函数,可以是virtual的也可以不是
  • 在C++中,当我们使用基类的引用或指针调用一个虚函数时,将发生动态绑定,实际执行函数根据实际类型而定
  • 任何构造函数之外的非静态函数都可以是虚函数
  • 子类能访问父类的public与protected成员,不能访问private成员
  • 首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员
  • 类名后加final可以阻止该类被继承,函数名后加final可以阻止该函数被子类覆盖
  • 只有指针类型和引用类型可以从派生类转换到基类,对象不可实现类型转换,但可以通过拷贝构造函数或拷贝赋值运算符由派生类对象产生一个基类对象
  • 只有通过指针类型或引用类型调用虚函数才能实现多态,通过对象调用函数会根据对象声明类型调用对应函数
  • 当子类覆盖父类的虚函数时,该函数在父类中的形参必须与子类的严格匹配
  • 虚函数总是采用基类中定义的默认实参
  • 可以使用p->Quote::net_price(4)来回避虚函数的机制,不管p的运行多态类型,总是调用基类Quote的net_price函数,相当于调用super.
  • 类内定义虚函数=0设为纯虚函数,含有纯虚函数的类为抽象类,不能构造抽象类的对象
  • protected成员为受保护的成员,受保护成员对于类的用户来说是不可访问的,对于派生类和友元来说是可访问的
  • 派生类或友元只能访问派生类对象继承下来的受保护成员,而不能访问基类对象的受保护成员
  • 派生访问说明符声明了基类的成员在派生类中是什么访问类型的,取原访问类型与派生访问说明符中权限较小的作为基类成员在派生类中的访问类型
  • struct默认公有继承,class默认私有继承
  • 只有当继承方式是公有的,用户代码才可以使用派生类向基类的转换,派生类中的成员和友元总是可以使用此类型转换,派生类的派生类中的成员和友元需要public或protected继承才能使用此类型转换
  • 友元不能继承,但基类的友元可以访问派生类中的基类成员
  • 派生类的作用域嵌套在基类的作用域之内,因此指针或引用无妨访问超过该类型的作用域内的成员(普通成员无动态绑定)
  • 基类通常应该定义一个虚析构函数,以实现动态分配继承体系中的对象被动态析构

 

第16章 模板与泛型编程
  • 默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化,使得即使不能完全符合模板操作的类型也可以实例化该模板
  • A文件include<B>文件,而在B中又先要引用A文件中的某些全局变量,可以使用关键字extern来在B文件中声明此变量在程序的其它位置已定义

posted @ 2018-04-18 10:47  LloydDracarys  阅读(88)  评论(0)    收藏  举报