C++ primer(第五版)中需要复习第二遍的知识点记录
原博客搬移到:https://blog.csdn.net/u013171226/article/details/107680406
工作中发现自己对C++掌握的不够扎实,于是打算利用业余时间系统的复习下C++,先看C++primer,看完C++ primer之后会挑一个质量比较好的C++学习视频,此笔记为阅读C++ primer(第五版)时的笔记。
第二遍需要复习的东西
第一章 开始
第一章没有需要复习的知识点。
第二章 变量和基本类型
2.2变量
C++中对一个变量进行初始化有好几种方式,例如下面的几种都是可以的。
int var = 1;
int var(1);
int var{1};
int var = {1};
C++11扩大了初始化列表的适用范围,使其可用于所有内置类型和用户定义的类型,适用初始化列表时,可添加=,也可以不添加。
另外,当初始化列表用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化值存在丢失信息的风险,则编译器将报错,例如:
long double ld = 3.141592653;
int a = {ld};//错误,转换未执行,因为存在丢失信息的风险。
int b(ld);//正确,转换执行,且确实丢失了部分值。
2.3.1引用
引用不需要去复习书,只需要记住C++引入引用的原因就好了,C++引入引用的原因“为了支持运算符重载”,详细一点就是,普通的参数传递实际上传递了一份拷贝,把实参拷贝给形参,这样当参数很大时,会导致开销比较大,而且普通的参数是对形参进行操作,不能修改实参,因此总结就是“节省开销,参数回写”。
2.4const限定符,这个有必要把2.4所有的都复习一遍。
2.5处理类型全部复习一遍。
第三章 字符串、向量和数组
3.2标准库类型string需要复习。
3.3标准库类型 vector挺简单的,不过还是复习一下吧,练习题不用做了。
3.4迭代器介绍,一共就4页,复习一下吧。
3.5数组 都比较简单,不需要复习。只需要复习3.5.3中的 标准库函数begin和end那一小点,五分钟就就看完了。
3.6 多维数组,不需要复习。
第四章 表达式
4.11类型转换 需要复习,
4.12 运算符优先级不需要复习,只需要记住 算数-关系-逻辑-条件-赋值-逗号
第五章 语句
5.4.3 范围for语句,需要复习。
5.6 try语句和异常处理,需要复习。
第六章 函数
6.2.3 尽量使用常量引用,函数的形参要定义成const类型,这样的话既能接收const类型参数,也能接受普通类型参数,记住这一句话就醒了,6.2.3小节不用复习。
6.2.6 含有可变形参的函数 这一小节复习一下。
6.3.3 返回数组指针
第七章 类
7.1.2 只需要记住, this是指向当前被调用的成员函数所在对象的起始地址, 成员函数都有一个隐式形参this.
常量成员函数实际上是修改了隐式this指针的类型。 常量对象只能调用常量成员函数,非常量对象既可以调用常量成员函数也可以调用非常量成员函数。
编译器首先编译成员的声明,然后再编译成员函数体,因此成员函数体可以随意使用类中的其他成员而不用在意这些成员出现的次序。注意是仅限于成员函数中使用的名字,而对于声明中出现的名字,包括返回类型或者参数列表中出现的名字,都必须在使用前确保可见。(先编译变量和函数的声明,再编译函数体)
7.1.4 只需要记住,构造函数的名字和类名相同,构造函数没有返回类型。
7.2只需要记住, public说明符之后的成员在整个程序内可以被访问,在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问。
使用struct关键字时,定义的成员默认是public的,使用class关键字时,定义的成员默认是private。
7.2.1友元
类把其他类或者函数声明为友元之后,则其他类或函数可以访问该类的非公有成员。友元声明只能出现在类定义的内部。
友元的声明仅仅指定了访问的权限,而不是通常意义上的函数声明,因此我们要在友元声明之外再专门对函数进行一次声明。
为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中。
7.4 类的作用域,只需要记住
一旦遇到了类名,定义的剩余部分就在类的作用域之内了,意思就是我们在类外面定义成员函数时,前面成员函数名字之前需要用类名和作用域运算符指明作用域,但是后面参数以及函数体里面用到类的数据成员时就不需要再加类名和作用域运算符了。
7.5.1 构造函数初始值列表是对数据成员进行初始化,一旦构造函数体开始执行,初始化就完成了,这时在成员函数中对参数进行赋值是赋值不是初始化,而有些引用以及const类型又必须初始化,所以要养成使用构造函数初始化列表的习惯,使用构造函数初始化列表还能提高效率。
成员的初始化顺序与他们在类定义中出现的顺序一致,而非初始值列表的顺序。
7.5.4隐式的类类型转换 /ɪkˈsplɪsɪt/
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称为转换构造函数。例如接受string的构造函数定义了从string向Sales_data的隐式转换的规则,也就是说在需要使用Sales_data的地方,我们可以使用string或者istream作为替代,例如
string null_book = "9-999-9999-9"; item.combine(null_book);
编译器只会自动地执行一步类型转换,例如item.combinr("9-999-9999-9");这行代码隐式地使用了两种转换规则,所以它是错误的。
我们可以通过将构造函数声明为explicit阻止隐式类型转换,例如 explicit Sales_data(const std::string &s):bookNo(s){};此时string null_book = "9-999-9999-9"; item.combine(null_book);将编译不通过。
关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit,只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。
由于执行拷贝形式的初始化时会发生隐式转换,因此用explicit声明的构造函数不能用于拷贝形式的初始化,只能用于直接初始化的方式,Sales_data item1(null_book);//正确, Sales_data item2 = null_book;//错误,explicit构造函数不能用于拷贝形式初始化。
7.6静态成员属于类,而不属于类的某个对象,虽然静态成员不属于类的某个对象,但是我们仍然可以使用类的对象、引用或者指针来访问静态成员。
静态数据成员在类内声明,在类外定义和初始化, static关键字只出现在类内部的声明语句,类的外部定义静态成员时,不能重复static关键字。
第八章 IO库
虽然没记住,但是这一章不重要,而且也比较简单,真正用的时候稍微复习一下就好了,所以第二遍不用看第八章了。
第九章 顺序容器
9.1 只需要记住,除非你有很有的理由选择其他容器,否则应该使用vector.
9.2.1 迭代器,begin()是指向容器中第一个元素的迭代器,end()是指向容器尾元素之后的位置,注意不是尾元素,另外begin()和end()都是迭代器,类似指针,要获取元素内容需要加*。
9.2.4 C sep(n, t); n个初始化为值t的元素。
9.2.5 顺序容器可以用assign, assign()函数的参数可以是一个迭代器范围,也可以是 (n, t), assign用参数所指定的元素替换容器中的所有元素。
swap交换两个相同类型容器的内容,元素本身并没有交换,其实可以理解成只是把容器名字交换了,因此指向容器的迭代器,指针,引用都没有失效,还是指向之前的那个元素,只不过交换之后这个元素属于另一个容器了而已。例如交换之前,某个指针指向vec1[3]这个元素,那么交换之后指针还是指向这个元素,只不过此时这个元素叫vec2[3]不再是vec1[3]l .
9.3顺序容器操作,注意9.3节所说的操作只有顺序容器才支持,
9.3.1 向顺序容器添加元素,
vector和string不支持push_front和emplace_front.但是他们都支持insert,所以完全可以用insert在容器的开始位置插入元素。
c.push_back(t) c.emplace_back(args)在c的尾部创建一个值为t或由args创建的元素,返回void.
c.push_front(t) c.emplace_front(args)在c的头部创建一个值为t或由args创建的元素,返回void.
c.insert(p, t) c.emplace(p, args)在迭代器p指向的元素之前创建一个值为t或由args创建的元素,返回指向新添加的元素的迭代器。
c.insert(p,n,t)在迭代器p指向的元素之前插入n个值为t的元素,返回指向新添加的第一个元素的迭代器,若n为零,则返回p.
c.insert(p,b,e)将迭代器b和e指定的范围内的元素插入到迭代器p指向的元素之前,b和e不能指向c中的元素,返回指向新添加的第一个元素的迭代器,若范围为空,则返回p。
c.insert(p,il)il是一个或括号保卫的元素值列表,将这些给定值插入到迭代器p指向的元素之前,返回指向新添加的第一个元素的迭代器,若列表为空,则返回p.
向一个vector 、string 或deque插入元素会使所有指向容器的迭代器引用和指针失效。
9.3.2 c.front()返回c中首元素的引用,c.back()返回c中尾元素的引用,注意是引用,而不是像迭代器那样的指针,并且back返回的是尾元素的引用,并不是尾元素的后一个元素。
c[n] c.at(n)这两个都是返回c中下标为n的元素的引用,区别在于c[n]如果下标越界,编译器也不会检查,而c.at(n)如果下标越界,那么会抛出一个异常。
9.3.3删除元素, vector和string不支持pop_front. 删除元素的成员函数并不检查其参数,在删除元素之前,程序员必须确保他们是存在的。
c.pop_back() 删除c中尾元素,函数返回void,若c为空,则函数行为未定义。
c.pop_front() 删除c中首元素,函数返回void,若c为空,则函数行为未定义。
c.erase(p) 删除迭代器p所指定的元素,返回一个指向被删元素之后元素的迭代器,若p指向尾元素,则返回尾后迭代器,若p是尾后迭代器,则函数行为未定义。
c.erase(b,e)删除迭代器b和e所指定范围内的元素,返回一个指向最后一个被删元素之后元素的迭代器,若e本身就是尾后迭代器,则函数也返回尾后迭代器。
c.clear()删除c中所有元素,返回void。
9.3.5改变容器大小
c.resize(n),调整c的大小为n个元素,若c<c.size(),则多出的元素被丢弃,若必须添加新元素,对新元素进行值初始化。
c.resize(n, t)调整c的大小为n个元素,任何新添加的元素都将初始化为值t.
9.3.6 迭代器失效, 前面几节课说的什么删除元素,插入元素会使迭代器失效,意思并不是进行这种操作之后迭代器就不能用了,意思是操作之后之前迭代器的值失效了,但是重新取迭代器还是一样可以的,例如insert之前如果我们保存了ietr = c.end(),那么insert之后iter就失效了,但是这时候如果我们取c.end()还是可以用的。
9.4 vector对象是如何增长的,
vector和string的实现通常会分配比实际空间需求更大的内存空间,容器预留这些空间,这样就不需要每次添加新元素都重新分配容器的内存空间了。
c.reerve(n),分配至少能容纳n个元素的内存空间,该函数并不会改变容器中元素的数量,只是影响vector预先分配多大的内存空间。当n大于当先预留内存空间时会扩大空间到n,如果当n小于预留空间,那么当前容量不会变,并不会退回到n。
c.capacity() 是指在不分配新的内存空间的前提下它最多能保存多少个元素, size()只是它已经保存的元素数目。
9.5额外的string操作
9.5.1构造string的其他方法
string类型除了具有其他顺序容器相同的构造函数外,还支持另外三个构造函数。
string(cp) s是cp指向的数组中字符的拷贝,这种写法要求cp指向的字符数组中要以空字符结尾,拷贝操作遇到空字符停止。
string(cp, n) s是cp指向的数组中前n个字符的拷贝,此数组至少应该包含n个字符。
string(s2, pos2) s是string S2从下标pos2开始的字符的拷贝,若POS2>s2.size(),则构造函数的行为未定义。
string(s2, pos2, len2) s是string s2从下标pos2开始len2个字符的拷贝,若pos2>s2.size(),构造函数的行为未定义,不管len2的值是多少,构造函数至多拷贝s2.size() - pos2个字符。
substr操作返回一个string,他是原始string的一部分或全部的拷贝,
substr(pos, n),返回一个string,包含s中从pos开始的n个字符的拷贝,pos的默认值为0,n的默认值s.size() - pos,即拷贝从pos开始的所有字符。
9.5.2改变string的其他方法
除了接受迭代器的insert和erase版本外,string还提供了接受下标的版本,下标指出了开始删除的位置,或是insert到给定值之前的位置
s.insert(s.size(), 5, 'i') //在s末尾插入五个感叹号
s.erase(s.size() - 5, 5)//在s删除最后5个字符
9.5.3 string搜索操作
每个搜索操作都返回一个string::size_type值,表示匹配发声位置的下标,搜索操作是大小写敏感的
s.find(args) 查找s中args第一次出现的位置。
s.rfind(args)查找s中args最后一次出现的位置,
s.find_first_of(args)在s中查找args中任何一个字符第一次出现的位置。
s.find_last_of(args)在s中查找args中任何一个字符最后一次出现的位置
s.find_first_not_of(args) 在s中查找第一个不在args中的字符。
s.find_last_not_of(args) 在s中查找最后一个不在args中的字符。
args必须还一下形式之一
c, pos 从s中位置pos开始查找字符c,pos默认为0.
s2,pos 从s中位置pos开始查找字符串s2,pos默认为0,
cp,pos,从s中位置pos开始查找指针cp指向的以空字符结尾的c风格字符串,pos默认为0,
cp,pos,n,从s中位置pos开始查找指针cp指向的数组的前n个字符,pos和n无默认值。
9.5.4compare函数, 这个不要用,string可以直接用== >= <=这些去比较,不要用compare函数进行比较。
第十章 泛型算法
泛型就是通用的意思,泛型算法就是指通用的算法。泛型算法就可以理解成是对容器提供的一系列通用的操作。
10.1 概述
一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作,通常情况下,算法遍历范围,对其中每个元素进行一些处理。例如,假定我们有一个int的bector,希望知道vector中是否包含一个特定值,回答这个问题最方便的方式是调用标准库算法find:find(vec.cbegin(), vec.cend(), val);
10.2初识泛型算法
int sum = accumulate(vec.cbegin, vec.cend(), 0);//对vec中的元素求和,和的初值是0.
fill(vec.begin(), vec.end(), 0)//将每个元素重置为0.
fill_n(vec.begin, vec.size(), 0);//将所有元素重置为0.
back_inserter: vector<int>vec; auto it = back_inserter(vec); *it = 10;back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器,当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到元素中,
我们常常使用back_inserter来创建一个迭代器,作为算法的目的位置来使用,vector<int> vec; fill_n(back_inserter(vec), 10, 0);添加是个元素到vec。
copy(begin.(a1), end.(1), a2);//a1和a2为两个数组,把a1的内容拷贝给a2.
10.2.3 重排容器元素的算法
sort(words.begin(), words.end());//按照字典顺序排序words.这样操作之后重复的单词就排序到相邻位置了。
auto end_unique = unique(words.begin(), words.ends);//unique会把words中重复的单词保留一个,另一个放到后面,unique返回的是不重复值范围末尾的迭代器,
例如一个英语单词的vector,内容为, the quick red fox jumps over the slow red turtle,用sort函数操作之后变为,fox jump over quick red red slow the the turtle.然后unique之后变为fox jumps over quick red slow the turtle ??? ??? ,unique返回的迭代器就是指向后面的那第一个问号,用问号是因为不确定后面放的是the red还是red the。 经过unique之后,我们再调用words.erase(end_unique, words_end());
10.3定制操作
很多算法在比较输入序列中的元素的时候,都是使用元素类型的<或==运算符来完成,但是我们可以提供自己定义的操作来代替默认运算符。
谓词是一个可调用的表达式,一元谓词意味着它们只接受单一参数,二元谓词意味着它们有两个参数。
10.3.2lambda表达式
我们可以向一个算法传递任何类别的可调用对象,可调用对象是指可以对其使用调用运算符的对象(调用运算符是一对圆括号),即,如果e是一个可调用的表达式,则我们可以编写代码e(args),其中args是一个逗号分隔的一个或多个参数的列表。lambda表达式就是一种可调用对象。
一个lambda表示一个可调用的代码单元,我们可以将其理解为一个未命名的内联函数。
一个lambda具有如下形式,[capture list](parameter list)->return type{function body}.其中捕获列表是一个lambda所在函数中定义的局部变量的列表,与普通函数不同,lambda必须使用尾置返回来指定返回类型。
10.4.1 插入迭代器
vector<int>vec; auto it = back_inserter(vec); *it = 10;back_inserter接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器,当我们通过此迭代器赋值时,赋值运算符会调用push_back将一个具有给定值的元素添加到元素中,
另外,front_inserter创建一个使用push_front的迭代器, inserter创建一个使用insert的迭代器,此函数接受第二个参数,这个参数必须是一个指向给定容器的迭代器,元素将被插入到给定迭代器所表示的元素之前。
第十一章 关联容器
关联容器支持高效的关键字查找和访问。
map :关联数组,保存关键字-值对。
set:关键字即值,即只保存关键字的容器。
multimap:关键字可重复出现的map。
multiset:关键字可重复出现的set。
11.2关联容器概述
关键字类型必须定义元素比较的方法,默认情况下,标准库使用关键字类型的<运算符来比较两个关键字。 我们也可以提供自己定义的操作来代替关键字上的<运算符。
11.2.3 pair类型
标准库类型pair定义在头文件utility中,一个pair保存两个数据成员,pair<string, string> anon; pair<string, size_t> word_count;
pair的两个成员分别命名为first和second,我们用成员访问符.可以对其进行访问,
实际上map中的一对{key value}就是相当于一个pair,map的每个元素就是一个pair对象。
make_pair(v1, v2);返回一个用v1和v2初始化的pair,pair的类型从v1和v2的类型推断出来。
11.3关联容器操作
key_type 此容器类型的关键字类型,
mapped_type 每个关键字关联的类型,只适用于map.
value_type 对于set,与key_value相同,对于map,为pair<const key_type, mapped_type>
对于set类型,key_type和value_type是一样的,set中保存的值就是关键字,在一个map中,元素是关键字-值对,即每个元素是一个pair对象,包含一个关键字和一个关联的值,由于我们不能改变一个元素的关键字,因此这些pair的关键字部分是一个const的。
我们使用作用域运算符::来提取一个类型的成员,例如 map<string, int>::key_type;
11.3.2添加元素
插入元素:c.insert(v) v是value_type类型的对象。例如word_count.inset({word, 1}); word_count.inset(make_pair(word, 1));
insert的返回值依赖于容器类型和参数,对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,告诉我们插入是否成功,pair的first成员是一个迭代器,指向具有给定关键字的元素,second成员是一个bool值,指出元素是插入成功还是已经存在于容器中,如果关键字已在容器中,则insert什么事情也不做,且返回值中的bool部分为false,如果关键字不存在,元素被插入到容器中,且bool值为true。
11.3.3 删除元素
c.erase(k)从c中删除关键字为k的元素,返回一个size_type值,表示实际删除的元素数量,如果返回为0,表示想要删除的元素不在容器中。
c.erase(p)从c中删除迭代器p指定的元素,p必须指向c中一个真实元素,不能指向c.end(),返回一个指向p之后的元素的迭代器,若p指向c中的尾元素,则返回c.end();
c.erase(b, e);删除迭代器对b和e所表示的范围中的元素,返回e。
11.3.4map的小标操作
我们不能对multimap进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。
c[k]返回关键字为k的元素, 如果k不在c中,那么会创建一个关键字为k的元素,并插入到map中,并对其进行值初始化。
c.at(k) 带参数检查,如果k不在c中,抛出一个out_of_range异常。
如果关键字还未在map中,下标运算符会添加一个新元素,这一特性允许我们编写出异常简洁的程序,但是如果我们只是想知道一个元素是否已在map中,但是不存在时并不想添加元素,在这种情况下,就不能使用下标运算符。
11.3.5访问元素
c.find(k) 返回一个迭代器,指向第一个关键字为k的元素,若k不在容器中,则返回尾后迭代器。
c.count(k) 返回关键字等于k的元素的数量,对于不允许重复关键字的容器,返回值永远是0或1.
c.lower_bound(k) 返回一个迭代器,指出第一个关键字不小于k的元素,
c.upper_bound(k) 返回一个迭代器,指出第一个关键字大于k的元素,
lower_bound返回的迭代器将指向第一个具有给定关键字的元素,而upper_bound返回的迭代器则指向最后一个匹配给定关键字的元素之后的位置,因此用相同的关键字调用lower_bound和upper_bound会得到一个迭代器范围。如果lower_bound和upper_bound返回相同的迭代器,则给定关键字不在容器中。
c.equal_bound(k) 返回一个迭代器pair,若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置,若k不存在,pair的两个成员均等于c.end();
第12章 动态内存
12.1动态内存与智能指针
为了更安全地使用动态内存,标准库定义了两个智能指针负责自动释放所指向的对象。
shared_ptr 允许多个指针指向同一对象。
unique_ptr 则独占所指向的对象。
一下操作是shared_ptr和unique_ptr都支持的操作:
shared_ptr<T> sp;
unique_ptr<T> up;空智能指针,可以指向类型为T的对象。
p 将p用作一个条件判断,若p指向一个对象,则为true;
*p 解引用p,获得它所指向的对象。
p->mem 等价于*(p).mem
p.get() 返回p中保存的指针,要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了。
swap(p, q)
p.swap(q) 交换p和q中的指针。
以下操作是shared_ptr独有的操作:
make_shared<T>(args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象。
shared_ptr<T>p(q) p是shared_ptr q的拷贝,此操作会递增q中的计数器,q中的指针必须能转换为T*。
p = q p和q都是shared_ptr,所保存的指针必须能相互转换,从操作会递减p的引用计数,递增q的引用计数,若p的引用计数变为0,则将其管理的原内存释放。
p.unique() 若p.use_count()为1,返回true,否则返回false。
p.use_count() 返回与p共享对象的智能指针数量,可能很慢,主要用于调试。
make_shared函数:最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,次函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr,与智能指针一样,make_shared也定义在头文件memory中。例如 shared<int> p3 = make_shared<int>(42); shared_ptr<string> p4 = make_shared<string>(10, '9');
当然,我们通常使用auto定义一个对象来保存make_shared的结果,这种方式比较简单,auto p6 = make_shared<vector<string>>();
我们可以认为每个shared_ptr都有一个关联的计数器,当我们拷贝一个shared_ptr时,计数器都会递增,当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减,一旦一个shared_ptr的计数器变为0,他就会自动释放自己所管理的对象。
有时程序使用动态内存是为了让多个对象能共享数据。
如果我们用new delete自己管理动态内存非常容易出错,坚持只使用智能指针就可以避免所有这些问题,对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放他。
12.1.3sared_ptr和new结合使用
接收指针参数的智能指针构造函数是explicit的,而explicit声明的构造函数不能用于拷贝形式的初始化,只能用于直接初始化,因此,shared_ptr<int> p1 = new init(1024);//错误,必须使用直接初始化形式, shared_prt<int> p2 (new int(1024));//正确,使用了直接初始化形式。
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放他所关联的对象。
12.1.5 unique_ptr
以下是unique_ptr特有的操作。
unique_ptr<T> u1; unique_ptr<T, D> u2; 空unique_ptr,可以指向类型为T的对象,u1会使用delete来释放他的指针,u2使用一个类型为D的可调用对象来释放他的指针。
unique_ptr<T, D> u(d); 空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete。
u = nullptr; 释放u指向的对象,将u置为空。
u.release() u放弃对指针的控制权,返回当前保存的指针并将其置为空。
u.reset() u.reset(q);释放u指向的对象,如果提供了内置指针q,另u指向这个对象,否则将u置为空。
与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr,当我们定义一个unique_ptr的时候,需要将其绑定到一个new返回的指针上,类似shared_ptr,初始化unique_ptr必须采用直接初始化方式。unique_ptr<int> p2(new int(42));
由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。
12.1.6 weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象也还是会被释放,
由于创建unique_ptr不会增加p的引用计数,因此unique_ptr指向的对象可能已经被释放掉了,因此我们不能使用weak_ptr直接访问对象,必须调用lock,此函数检查weak_ptr指向的对象是否仍存在,如果存在,lock返回一个指向共享对象的shared_ptr。
12.2 动态数组
C++语言和标准库提供了两种一次分配一个对象数组的方法,C++语言定义了另一种new表达式,可以分配并初始化一个对象数组,标准库中包含一个名为allocator的类,允许我们将分配和初始化分离,
大多数应用应该使用标准库容器而不是使用动态数组,使用容器更为简单更不容易出现内存管理错误并且可能有更好的性能。
初始化动态分配对象的数组,int *pia3 = new int[10]{0, 1, 2, 3, 4, 5};
12.2.2 allocator类
new有一些灵活性上的局限,它将内存分配和对象构造组合在了一起,
标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来,它分配的内存是原始的,未构造的。类似vector,allocator是一个模板,为了定义allocator对象,我们必须声明这个allocator可以分配的对象类型,
allocator<string> alloc; //可以分配string的allocator对象,
auto const p = alloc.allocate(n);//分配n个未初始化的string
为了使用allocate返回的内存,我们必须用construct构造对象,使用未构造的内存,其行为是未定义的。
当我们用完对象后,必须对每个构造的元素调用destroy来销毁他们,函数destroy接受一个指针,对指向的对象执行析构函数,注意:只能对真正构造了的元素进行destroy操作。
while(q != p)
alloc.destroy(--q);
12.3 使用标准库:文本查询程序
第13章 拷贝控制
13.1 拷贝 赋值与销毁
13.1.1 拷贝构造函数
如果构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数,拷贝构造函数的第一个参数必须是一个引用类型。
class Foo{
public:
Foo();//默认构造函数
Foo(const Foo&);//拷贝构造函数
....
}
拷贝初始化何时发生
拷贝初始化不仅在我们使用=定义变量时会发生,在下列情况下也会发生。
1.将一个对象作为实参传递给一个非引用类型的形参。
2.从一个返回类型为非引用类型的函数返回一个对象。
3.用花括号列表初始化一个数组中的元素或一个聚合类中的成员,
某些类类型还会对它们所分配的对象使用拷贝初始化,例如,当我们初始化标准库容器或是调用其insert或push成员,容器会对其元素进行拷贝初始化,与之相对,用emplace成员创建的元素都进行直接初始化。
拷贝构造函数被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型,如果其参数不是引用类型,则调用永远也不会成功,为了调用拷贝构造函数,我们必须拷贝它的实参,但是为了拷贝实参,我们又需要调用拷贝构造函数,如此无线循环。
13.1.2拷贝赋值运算符
运算符重载本质是函数,名字是:operator运算符,例如重载的赋值运算符就是一个名为operator=的函数。
赋值运算符通常应该返回一个指向其左侧运算对象的引用。
1.1.3 析构函数
构造函数初始化对象的非static数据成员,析构函数释放对象使用的资源,并销毁对象的非static数据成员。
析构函数成员函数,名字是波浪号类名,它没有返回值,也不接受参数。
由于析构函数不接受参数,因此它不能被重载,对于一个给定类,只有一个唯一的析构函数。
在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照他们在类中出现的顺序进行初始化,在一个析构函数中,首先执行函数体,然后销毁成员,成员按照初始化顺序的逆序销毁。
通常,析构函数释放对象在生存期分配的所有资源。
当一个对象被销毁时,就会自动调用其析构函数。
1.变量离开其作用域时被销毁,2.当一个对象被销毁时,其成员被销毁,3.容器被销毁时,其元素被销毁,4.对于动态分配的对象,当对指向它的指针用delete运算符时被销毁。5.对于临时对象,当创建她的完成表达式结束时被销毁。
由于析构函数自动运行,我们的程序可以按需要分配资源,而无须担心何时释放这些资源。
认识到析构函数体本身并不直接销毁成员是非常重要的,成员是在析构函数体之后隐含的析构阶段中被销毁的,在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。
13.1.4 三/五法则
如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。
如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符,反之亦然,如果一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数,然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数。
13.1.6 阻止拷贝
对于某些类来说,拷贝和拷贝复制没有合理的意义,这种情况下,我们必须采用某种机制阻止拷贝或赋值。
在新标准下,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。删除的函数是这样一种函数,我们虽然声明了它,但是不能以任何方式使用他们,在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的。
=delete通知编译器,我们不希望定义这些成员。
struct NoCopy{
NoCopy()=default;//使用合成的默认构造函数
NoCopy(const NoCopy&) = delete;//阻止拷贝
}
析构函数不能定义为删除的函数,如果析构函数定义为删除函数,就无法销毁此类型的对象了,
13.2 拷贝控制和资源管理
13.5动态内存管理类
13.6对象移动
右值引用就是必须绑定到右值的引用,右值引用只能绑定到一个将要销毁的对象,通过&&获得右值引用。
一个左值表达式表示的是一个对象的身份(在内存中的位置),一个右值表达式表示的是对象的值(内容)。
左值和右值的区别:左值有持久的状态,右值是字面常量,或者是在表达式求值过程中创建的临时对象。
右值引用的两个特性:1.所引用的对象将要被销毁,2.该对象没有其他用户。
变量表达式都是左值,因此我们不能把一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型 也不行。
int &&rr1 = 42;//正确,字面常量是右值
int &&rr2 = rr1;//错误,表达式rr1是左值,
表达式左值,字面常量是右值。
标准库move:int &&rr3 = std::move(rr1);//OK ,调用move意味着承诺:除了对rr1赋值或销毁外,我们将不再使用它,也就是我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用它。
13.6.2 移动构造函数和移动赋值运算符
拷贝构造函数:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数,
移动构造函数:如果一个构造函数的第一个参数是自身类类型的右值引用,且任何额外参数都有默认值,则此构造函数是移动构造函数。
个人理解:移动构造函数其实就是相当于不重新申请内存,而是用源对象的指针初始化新对象的指针。
noexcept:其实就是告诉编译器,我们的移动构造函数是安全的,不用为我们做额外工作。
只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员默认地被定义为删除的。
第14章 重载运算与类型转换
14.1基本概念
运算符重载本质是函数,其名字是operator运算符。
如果一个重载运算符函数是成员函数,那么它的第一个运算对象绑定到隐式的this指针上,因此成员运算符函数的参数数量比运算符的运算对象总数少一个。
14.2输入和输出运算符
输入输出运算符(<< >>)必须是非成员函数,因为输入输出运算符的左侧对象是ostream类对象os或istream对象is。
14.3算数和关系运算符
14.5下标运算符
14.6递增和递减运算符
14.8函数调用运算符
如果类定义了调用运算符,则该类的对象称为函数对象,因为可以调用这种对象,所以我们说这些对象的行为像函数一样。
14.8.3可调用对象与function
可调用对象是一个模板,
14.9重载,类型转换与运算符
第15章 面向对象程序设计
15.1 OOP概述
面向对象程序设计的核心思想是数据抽象、继承和动态绑定。通过使用数据抽象,我们可以将类的接口与实现分离,使用继承,可以定义相似的类型并对其相似关系建模,使用动态绑定,可以在一定程度上忽略相似类型的差别,而以统一的方式使用他们的对象。
在C++中,基类将类型相关的函数与派生类不做改变直接继承的函数区别对待,对于某些函数,基类希望它的派生类各自定义适合自己的版本,
C++11新标准允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个override关键字。
在C++语言中,当我们使用基类的引用或指针调用一个虚函数时,该引用将被动态绑定,根据引用或指针所绑定的对象类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。
15.2定义基类和派生类
对于基类中的普通成员函数,派生类会直接继承而不会发生任何改变,对于基类中的用virtual 声明的函数是虚函数,派生类需要对虚函数重新定义以覆盖从基类继承而来的旧定义。
关键字virtual 只能出现在类内部的声明语句之前而不能用于类外部的函数定义,如果基类把一个函数声明为虚函数,则该函数在派生类中隐式地也是虚函数。
派生类可以继承基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员,和其他使用基类的代码一样,派生类能访问基类的公有成员,而不能访问基类的私有成员。
基类中的受保护成员(protected):派生类可以访问,但是其他用户不能访问,
作者:cumtchw
出处:http://www.cnblogs.com/cumtchw/
我的博客就是我的学习笔记,学习过程中看到好的博客也会转载过来,若有侵权,与我联系,我会及时删除。