C++ Primer 读书笔记

来源:互联网 发布:手机淘宝二维登录 编辑:程序博客网 时间:2024/06/07 09:22
1,命令编译生成的默认输出文件(可执行文件) 命名为:a.out(Unix), a.exe(Windows)
2,cout输出首先会存到缓存中,而printf之类的输出会直接输出到输出流中。
3,可以从键盘上输入End-Of-File:Ctrl+d(Unix), Ctrl+z(Windows)。
4,C++中最常见的三种编译错误:1)类型错误,将值赋给不同类型的变量;2)声明错误,使用未声明的变量,或者在同一个域中重复声明一个变量;3)语法错误,写错语法。
5,可以通过command <infile >outfile实现输入输出重定向。
6,C++中,内置类型的大小(sizeof)因机器不同可能会有所差异,但是相对大小是确定的,比如int的大小不会小于short的大小。sizeof所求的大小以字节为单位。
7,C++中字符的几种类型:
     char最基本的字符类型,8bits,可以存储一些基本的字符。
     wchar_t,16bits,可以存储计算机最大的扩展字符集。
     char16_t, 16bits,用于存储Unicode编码字符。
     char32_t, 32bits,同上。
8,char也分signed和unsigned。
9,浮点转整形,并不是通过向下或者向上取整,而是无论正负,直接将浮点部分舍去,只保留整数部分。
10,如果试着将一个超出范围的值赋给一个有符号类型的变量,将会导致这个变量undefined,不能直接使用。
11, 有的时候为了节省内存,可以用char存储整数并进行运算,但是char在有的机器默认是有符号的,而另一些机器是默认无符号的,所以使用之前最好指定是 否是有符号类型;但是,不要用bool存储整数和进行算数运算,绝大部分机器,会将赋值给bool类型变量的整数转换为0和1,所以用bool存储整数和 进行算数运算几乎可以确定会出错。
12, 用转义方式表示字符:\x...十六进制,\...八进制。
13,在C++中,初始化和赋值是有区别的,初始化是变量被创建时的第一次赋值(如果没有显示初始化则会被初始化为默认的值)。比如子类可以给从父类继承的变量直接赋值,但是不能直接对它初始化。
14, 变量的初始化:可以通过int a = v; int a(v); int a{v}; int a = {v};前两者较为常见,后两者是新标准,成为list初始化,这种初始化,当遇到可能丢失信息的初始化时,会报编译错误。list初始化可以用来初始化 vector。
15,C++中变量,函数内定义的变量默认初始化值是undefined,而函数外定义的变量默认初始化值是0.
16, 声明和定义,定义是一种声明形式,但是除了初始化它还会给变量申请一块内存,并且可能给其赋值。一个变量可以被声明多次,但只能被定义一次。通常用 extern来声明变量,如果用了extern的同时还给变量赋值,就会把extern的效果覆盖,变成了定义(extern int a = 0;);并且,如果在函数内用了extern声明,就不能同时对变量赋值(比如这种形式extern int a = 0;),否则会报编译错误。
17,引用必须被初始化,并且只能和一个变量绑定,一旦绑定了就不能在和其他变量绑定。比如int &a = b; 而int &a;和int &a = 0;就不对。
18,与引用不同,指针可以指向多个不同的变量,保存变量的所在地址。
19, 引用不是变量,所以指针不能指向引用。但是引用可以引用指针,比如int *p; int *&r = p;这个例子中,p是一个整型指针,r是指针p的一个引用。遇到引用和操作符多个连用的情况,可以从右到左逐个看,最右边的就是变量的真正类型。比如上述 例子中,r是个引用,他所引用的类型是一个指针p,p是指针,它所指向的内容是一个整数。
20,const 常量:const常量在编译时就会被初始化,每次引用const常量在编译期间都会被替换成常量值。但是,const常量是Local to file的,一般情况下只能在被定义的文件中使用。正因为const常量是Local to file的,在不同文件定义相同名字的常量会被视为在这些文件里面分别定义,不会冲突,所以可以在多个文件定义同一个常量,但是这样会导致代码冗余,如果 一个常量需要修改就会导致多个文件的修改,并且如果该常量的初始化部分不是常数表达式,会导致多次非常数时间的计算,可能会影响效率。一个正确的方法是: 定义一次,声明多次。如果一个const变量要在多个文件使用,在定义时必须加extern。比如在file.cpp里面定义extern const int a = f(); 在file.h可以这样声明:extern const int a;这样就可以正常使用了。
21,const引用,const引用可以“绑定”非初始化变量,限制比非const引用更少:
比 如,double d = 1.2; int &a = d;这条语句会报错,原因是非const引用只能引用同种类型的变量。而const引用却不是这样的,比如const int &ca = d; 甚至const int &ca  = d*2;都是合法的。原因在于,对const引用初始化时,编译器自动生成一个中间变量,比如const int &ca = d;会被编译器自动转成以下代码:double d = 1.2; const int tmp = d; const int &ca = tmp; tmp是一个临时变量,系统生成的,没有名字,因此程序员不能够通过名字去访问。
22,Top- Level const 和 Low-Level const:通常出现在定义指针中,Top-Level描述指针本身的,Low-Level是描述指针所指向的值。比如const int * a = NULL;中的const是Low-Level的,他标识该指针所指向的值不能被该指针修改。int *const a = NULL;中的const是Top-Level的,他表示该指针本身不能被修改。引用是没有Top-Level const的,因为他本身不是对象。在拷贝的过程中会忽略Top-Level const。
23,constexpr int a = f();标识a的初始化值必须是一个常量表达式,并且一旦初始化以后就不能再改变。
24,auto i  = a;会根据i的初始化值的类型自动确定i的类型。decltype(a) b = 0; 会根据a的类型来定义b。
     两者的区别:auto会推断目标变量(引用或者指向)的类型,decltype则会推断目标变量的类型。比如int &r = i; auto a = r;此时的a是int类型的;而int &r = i; decltype(r) i = a; r却是int&类型的。auto会忽略Top-Level const,而decltype则不会忽略。decltype((i))得到的结果是i对应类型的引用。
25,using a = int; C++11中定义类型的方式。
26,一般来说,头文件里面不要用using namespace ...;因为引入头文件的地方会头文件copy一份到本文件,所有引用该头文件的地方都会using 这个namespace,可能会导致命名冲突。
27,为了兼容C,C++中的字符串常量和string是不一样的,并且在字符串+运算中,必须有一个运算数为string的。
28,vector可以用list初始化,也可以用自带的初始化函数:(n, s);n是初始化size,s是初始化值(可选)。
29,数组之间不可赋值。
30,没有引用数组,只有对数组的引用。int &refs[10] = ...;是错误的;而int (&arrRef) [10] = ...;是对的。
31,int *ptrs[10];是由10个指针组成的数组;而int (*Parray)[10];则是指向对由10个元素组成的数组的指针。
32,字符数组切记要有空结束符'\0'。
33,*p实际上是对指针p指向对象的引用。
34,一般的操作符并不能保证操作数的evaluate的顺序,比如int a = fa()+fb();就不能确定是哪个函数先执行。但是有四种操作符能够确保操作数的evaluate的顺序,分别是:&&, ||, ? :, ,(逗号)。
35,在新的版本中,整数除法如果有余数,结果无论正负都向靠近0的方向舍入(truncated)。对于带有负号的除法或者取模运算,如果是除法,将负号提出来。如果是取模,分情况讨论,m%(-n) = m%n,(-m)%n = -(m%n);
36,取引用的优先级低于后自增,比如 *ptr++ = *(ptr++);, 也低于成员访问符,比如*p.size()是错误的,应该是(*p).size();
37,这种形式的用法要注意,*ptr = f(*ptr++);这条语句是undefined的。
38,类型转换的基本规则:小转大,有符号转无符号。
39,switch 的分支值必须是常数表达式。switch的每个分支必须要加break。不然后面的条件包括该条件下的其他语句也会执行,即使是最后一个分支,都最好加 break;每个switch最好加上default,即使default情况下不做什么处理,也最好加上,这样做可以表明写程序时已经考虑到这种情况 了。case语句中:不能给变量初始化,除非case语句加上{}。
40,C++中如果异常没有被捕获,就会自动调用系统程序terminate终止程序。
41,C++中函数不能返回数组和函数,但是可以返回他们的指针。
42,传二维数组需要指定第二维的大小,尽量用这种:void f(int (*matrix)[10], int rowSize) {}, 而尽量不要用void (f(int matrix[][10], int rowSize) {}。
43,int main(int argc, char *argv[]) {},argc是用户输入参数的个数,而argv的大小是argc+1个,其中argv[0]是程序名称,后面argc个才是用户输入的参数。
44,函数可变数量的参数,可以用新版本的initializer_list<T> l;传递,也可以用省略号(...)传递,如果不是考虑兼容C,应该选择用initializer_list传递。
45,C++中返回数组指针的几种方式:
     1) typedef int arrT[10]; / using arrT = int[10];
          arrT* func(int i);
     2) int (*func(par_list)) [dimension];
     3) auto func(int i) -> int(*) [10];
     4) decltype(arr) * arrPtr(int i);
46,最好不要在局部域内声明函数,否则会将外部的同名函数都hide起来。
47,函数默认参数必须从左到右顺序声明,也就是说,第n个参数声明了默认值,那么第n+1个参数必须声明默认值。并且默认参数的默认值不一定要是常数或者常量表达式。
48,inline(内联)函数被调用时,不需要像一般函数调用那样(保存寄存器信息,跳到新的地方执行,返回病恢复寄存器信息),而是直接讲函数内部的代码复制到调用出去执行。并且inline函数必须在调用它的每个文件里面定义,对于同一个程序的不同文件,如果inline函数出现的话,其定义必须相同。是否支持递归inline取决于编译器。inline函数最好定义在头文件里面。
49,constexpr函数要求返回类型和参数类型都为常数表达式,但是对于具体的值可以不是常数表达式。
50,可以在文件开头处添加:#define NDEBUG 表示不需要调试,所以文件中的所有调试语句(assert)都不会执行。
51,函数返回函数指针的方法:
     1) using F = int(int*, int); F* f1(int); 或者 using PF = int(*) (int*, int); PF f1(int);
     2)  int (*f1(int)) (int*, int);
     3) auto f1(int) -> int (*) (int*, int);     (trailing return)
52,定义在类中的函数隐式地为inline函数。
53,对象成员函数的const加载方法参数列表后面,表示对象的this是一个指向const变量的const指针,指针本身和指针指向的值都不能修改。并且const对象只能访问对象的const方法。
54, 默认构造函数:如果一个类没有定义构造函数,编译器会自动生成一个默认的(无参数)构造函数。如果类添加了新的构造函数,又想继续保留默认构造函数,可以 自己定义,或者在新标准下还可以ClassName() = default;来定义默认构造函数,这标识这个默认构造函数使用系统自动生成的。
55,还可以对成员变量声明mutable,比如mutable int a;这表示变量a是可变的,即使在const函数里面,也是可以修改。
56,成员函数的定义在编译器处理所有类的声明之后处理。
57,类中,变量声明,首先看成员函数内部变量的声明,查看类成员变量声明,成员函数定义前的作用域。
58,类的成员变量的初始化顺序和成员变量在类中的定义顺序一致。
59,所有参数都有默认值的构造函数也可以用作默认构造函数。在C++11中还支持delegating 构造函数,比如ClassName(): ClassName(0, "", 1) {}
60,用默认构造函数定义变量应该是ClassName obj; 而不是ClassName obj(); 后者是声明一个返回ClassName对象的函数。
61,类或结构可以使用list initializer,但是初始化顺序必须和成员变量定义的顺序一致,并且所有成员变量必须是public的。
62, 类的构造函数没有const类型,但是可以在类的构造函数声明前面加constexpr,如果在普通函数声明前加constexpr,则说明这个函数只能 有return语句,但是构造函数没有返回类型,就是说它不能返回,所以加了constexpr的构造函数的函数体为空,成员变量的初始化只能用变量自带 初始化器,比如constexpr ClassName(int a): a(1) {};  也可以定义为default构造器,使用自动生成的默认构造器。
63,类的静态变量,可以用于类成员函数的默认参数,还可以在类里面定义与该类同类型的变量,比如                 ClassName {
    public: static ClassName A; //但是ClassName A;却是错误的。
     }
64,cout 输出首先会存储到缓存里面,到一定的时间会讲缓存中的数据flush从而打印到目标输出地址,这可能会引发一个异常情况,如果程序突然crash掉,但是 cout的缓存还有没有flush的内容,那么在目标输出地址就看不到这些内容。为了避免这种情况,可以通过一些方式强制将cout缓存中的内容 flush出来:
     1) cout << flush;
     2)cout << endl;
     3) cin >> ... ;  //cin和cout是tie关系,当cin出现的时候,cout的缓存会立即flush。可以通过设置tie关             系改变这类效果。
     流对象不能赋值,不能传参数,也不能当做返回类型,但是可以传递或者返回他们的引用。
65,可以通过sstream头文件里面的stingstream来读写string里面的内容:
     1)读s中的内容,istringstream = s; istringstream >> a;    
     2)写s中的内容,ostringstream = s; ostringstream << a;
     ostream和instream不能被深拷贝。
66,迭代器支持算数运算的容器包括:vector, string,deque, array。
67, 除了array以外的其他容器,都有自己的默认构造器,并且默认初始化为空;并且除了array以外的顺序容器,他们还支持指定size和初始值的初始 化,非顺序容器并不支持指定size初始化。为了避免不同size array之间的赋值,array不支持大括号list赋值。比如array<int, 10> A = {0, 1, 2, 3}; A = {1, 2}; 第二句是不对的。
68,容器可以通过拷贝其他容 器来初始化,如果直接通过A来对容器B初始化,则需要两个容器的类型以及容器内包含的元素都相同;比如vector<int> A = {1, 2, 3}; vector<int> B(A); 但是如果通过迭代器的方式来初始化,限制则没那么严格,比如list<const char*> A = {"ab", "ba"}; vector<string> B(articles.begin(), articles.end());,不需要容器类型相同,只需要容器内元素类型之间可以转换。类似地,容器可以通过调用assign来赋值,比如 list<int> A; list<int> B; B.assign(A);这种方法A和B的类型以及他们内部元素的类型都要相同;或者list<int> A; list<int> B; B.assign(A.begin(), A.end());这种方法不需要A和B的类型相同,只需要保证A中的元素类型可以想B中的元素类型转换,务必确保assign里面没有自身的迭代器。还 可以list<string> A; A.assign(10, "yes");
69,array的参数模板是<type, size>,比如array<int, 42> A; 而 array<int> 就是错误的。
70, 相同容器,并且容器包含的元素类型相同,那么两个容器可以相互比较,具体实现大概是逐个比较容器内的元素大小来确定大小关系,有序容器(除了 unordered_map和unordered_set等以外的容器)则会按照元素顺序来进行对比。当然,要确保容器中的元素支持比较操作。
71,除了array以外的顺序容器,支持insert操作:
     1) c.insert(p, t);在p元素前面插入一个元素t,返回指向元素t的迭代器,p也是一个迭代器。
     2)c.insert(p, n, t); 在p元素前面插入n个职位t的元素,返回这n个元素的第一个元素。
     3)c.insert(p, b, e);在p元素前面插入一段元素,这一段元素有两个迭代器b和e指定,b和e都不能是c中的迭代器。
     4)c.insert(p, {1, 2, 3});在p元素前面插入一段有大括号list组成的元素,返回插入元素的第一个元素所对应的迭代器。
     如果没有插入元素,则返回迭代器p。
72,list和forword_list都是从前面(front)插入元素。
73, 在C++11中,为容器新添加了一种操作emplace,可以在容器指定位置位置插入一个新的元素,传参的类型和容器内部元素类型的构造函数的形参一致。 emplace后,容器会为新元素分配一块空间,并将新元素插入到指定位置。而一般的插入,是新建一个局部临时变量,然后将这个元素插入到容器中。通常的 插入主要有三种emplace操作:
     1)c.emplace_back(args); 效果类似c.push_back。
     2)c.emplace_front(args);效果类似c.push_front。
     3)c.emplace(args);效果类似insert。
     只是类似,但内存分配上有差异。
74, 对vector或者string的迭代器,指针以及引用,一旦容器存储空间被重新分配,他们就会失效。如果存储空间没有被重新分配,但是有新的元素插入, 那么插入元素前的迭代器(或者指针,引用)保持有效,但是插入元素后面的迭代器(或者指针,引用)会失效。其他容器也会有类似的情况,所以在使用对容器的 迭代器,指针和引用的时候要考虑他们是否会失效。
75,对于线性容器,有三个和容器容量有关的函数:
     1)shrink_to_fit():请求容器释放空间,使得容器的空间刚好够存储已有元素。
     2) capacity():计算容器的容量,不是已有元素的个数。
     3) reserve(n):为容器预分配n个元素的空间,不会影响容器已有元素的个数,reserve的空间小于当前容器的空间,reserve失效。
76,Lambda 表达式:lambda表达式是一个callable对象,当一个lambda表达式被定义的时候,编译器会它自动生成一个匿名类,当lambda表达式被 传入一个函数的时候,编译器会自动定义一个lambda表达式的类对象。它可以用作实现简单,并且使用次数和地方少的情况。表达式格式如下:
     [capture list] (parameter list) -> return type { function body }
     capture list是一些lambda表达式函数的局部变量,通常为空,但是不能省略。
     lambda必须通过trailing return方式来指定返回类型。
     在lambda表达式中,参数列表和返回类型可以省略,但是capture list和函数体不能省略。
     比如 auto f = [] { return 42; };     cout << f() << endl;
     如果lambda表达式有参数列表,可以这样写:
     [] (const int &a, const int &b) {return a < b; }
     lambda表达式也可以当做一个函数的参数使用,比如可以用lambda表达式实现sort函数的比较函数:
     sort(nums.begin(), nums.end(), [] (const int &a, const int &b) { return a < b; });
     capture list 用来只用来声明非静态局部变量,比如:
find_if(nums.begin(), nums.end(), [threshold] (const int & a) {return a < threshold;});
capture list 可以capture一个变量的引用,但是要确保在使用这个变量的引用时,这个变量存在。capture list有以下几个用法:
     1)[], 空capture list。
     2)[names],capture多个变量,以逗号分隔。
     3)[&],隐式地将lambda表达式函数体所使用的变量的引用capture。
     4)[=],隐式地将lambda表达式函数体所使用的变量capture。
     5)[&, identifier_list],隐式地将lambda表达式函数体所使用的变量的引用capture,除了identifier_list包含的变量([&, a, b]),lambda表达式直接使用这些变量本身。
     6)[=, reference_list],隐式地将lambda表达式函数体所使用的变量的引用capture,除了reference_list包含的变量,这 些变量以引用的方式列出来([=, &a, &b]),并且以引用的方式被使用。
     lambda表达式可以直接使用局部静态变量,不需要在capture list里面声明。
     lambda表达式默认是不能改变capture list的元素,但可以在函数体前面声明mutable,表示函数体可以改变capture list的元素。
77,bind函数:bind是functional头文件里面的一个函数,用于绑定函数与参数。表达式形如:
     auto newCallable = bind(callable, arg_list);
     bind的第一个参数是原函数本身,arg_list的形式也和callable一致,但是可以为arg_list绑定具体的值或者placeholders。比如:
     auto f1 = bind(f0, a, b, std::placeholders::_2,std::placeholders:: _1);
     此时如果要调用f1,需要传入两个参数,并且当f1被调用时,就相当于调用f0,并且将a当做f0的第一个参数,b当做第二个参数,f1的第二个参数当做f0的第三个参数,f1的第一个参数当做f0的第四个参数,传入f0。
78,在C++11中,map和set也可以用list initialization,比如:
     set<string> S = {"the", "but", "and"};
     map<string, string> M = { {"1", "one"}, {"2", "two"}, {"3", "three"}};
79,Associative Container的insert(或者emplace)函数:用于向容器中插入元素,返回值是一个pair,pair的第一个参数是给定key对应的迭代器,pair的第二个参数是一个bool类型的参数,表示是否插入成功。
80,multiset 和multimap:与set和map类似,但是在multiset和multimap中,一个key可以对应多个value。查询的时候,会将给定 key值查到的第一个元素的迭代器返回。如果想知道给定key值在multi容器中有几个对应的元素,可以使用c.count(key)方法。如果想遍历 multi容器中给定key值定义的元素,有三种方法:
     1)可以先使用c.count函数计算出元素个数,然后使用c.find(key)找到第一个元素,往后递推c.count-1个元素即可遍历全部所需元素。
     2)使用c.equal_range(key),该函数返回两个迭代器,表示key值对应元素的起始迭代器(闭区间)和结束迭代器(开区间),然后从起始迭代器遍历到结束迭代器即可。
     3)使用c.upper_bound(key)和c.lower_bound(key),lower_bound求key值不小于给定值的第一个元素对应 的迭代器,upper_bound(key)求key值大于给定值的第一个元素对应的迭代器。然后通过这两个函数返回的迭代器遍历即可。
81,对于unordered容器,如果key值是自定义的,那么需要在自定义类型实现hasher和eqOp两个函数。
     比如:
     size_t hasher(const ClassName &obj)
     {
         return hash<string>() (obj.tostr());
     }
     bool eqOp(const ClassName &obj0, const ClassName &obj1)
     {
         return obj0.tostr() == obj1.tostr();
     }
82, 在C++中,new分配变量空间时有点需要注意:T v = new T(),和T v = new T是不同的,前者是变量初始化,后者是默认初始化。所有类型都定义了变量初始化,自动将变量初始化为“空”,这个“空”对于不同类型对应不同的值,整数是 0,浮点是0.0,字符串是“”。但是默认初始化就取决于类型是否有默认构造函数,如果有构造函数,那么就会调用默认构造函数初始化v,否则v的值就是 undefined。build-in类型没有默认构造函数,所以T v = new T,会使得v所指向的值为undefined。
83,delete只能对普通指针使用,不能对一般变量使用。
84, 智能指针主要是在普通指针的基础上封装了一层,使得使用者对指针的使用更加方便和放心,在使用的过程中不用担心指针因为释放问题而导致的异常。在 C++11中,智能指针主要有三种:shared_ptr<T> ptr, unique_ptr<T> ptr, weak_ptr<T> ptr;
     shared_ptr<T> ptr的初始化可以通过以下几种方式:
     1)shared_ptr<T> ptr = make_shared<T>(args); //args的参数形式和T的构造函数一致。
     2)shared_ptr<T> ptr(q); //q可以是一个智能指针或者普通指针(转换成T*的也行,比如&v),还能是一块新分配的内存。
     3)shared_ptr<T> ptr = q; //q可以是智能指针或者一般指针,但不能是一块新分配的内存。
     4)shared_ptr<T> ptr(u); //u是一个unique_ptr,此时u被被置位null,ptr指向u之前所指向的对象。
     5)shared_ptr<T> ptr(q, d); //和方法2)类似,但是会用新的删除函数d取代delete
     但是智能指针最好不要和不同指针混着用。
     其它一些操作,如:
    1)ptr = q;     //ptr指向q指向的对象,q必须是智能指针。ptr和q的模版类型不一定要完全一样,只要可以从q的模版类型转向ptr的模版类型即可。此时ptr原来指向的对象引用计数减1,q指向的对象引用计数加1。
    2)ptr.unique();//返回ptr所指向的对象引用数是否为1。
    3)p.use_count(); //后者返回ptr所指向对象引用数。
    与一般指针相比,shared_ptr主要可以用来防止内存泄漏和悬挂指针:
    1)内存泄漏是指动态分配的内存被遗忘释放了,导致这块内存一直不能被回收,一般较难检测出来,除非泄漏的内存非常多导致程序内存不够用了。如果使用普通 指针,就可能导致这个问题,比如:T *p = new T(); T *p= new T(); 此时p第一次指向的内存块就泄漏了。还有一种情况也可能导致内存泄漏:
          T* f(T arg)
           {
              T *ptr = new T(arg); 
              ...;
           }          //此时如果ptr不主动释放他所指向的内存,等到出了函数f的范畴,ptr指向的空间就会出现内存泄漏。
    而智能指针本身也是一个对象,它有自己的析构函数,当智能指针失效时,它会自动调用析构函数,删除自身,并将所指向对象的引用值减1,引用值为0就会删除所指向对象。
    2)悬挂指针是指向一块内存空间,这块内存空间之前保存一个对象,但是后来因为delete其他指针时被删除了,此时该指针就是悬挂指针。而使用智能指针,当删除一个智能指针时,首先将指向对象的引用值减1,如果此时引用值为0才会删除该对象。
    ptr.reset(); //是指ptr不再指向之前所指向的对象。
    ptr.reset(q); //是指ptr不在指向之前所指向的对象,转而指向q所对应或指向的空间。
    ptr.reset(q, d); //同上,只是用d替换了delete函数,在必要时,会调用d删除之前所指向的对象。
    使用智能指针需要注意以下几点:
    1)不要使用不同指针初始化或者reset多个智能指针。
    2)不要delete从get()返回的指针。
    3)不要使用get()返回的指针去初始化其它智能指针。
    4)如果使用了get(),需要注意,智能指针将所指空间内容删除时,会使得get()返回的对象指针成为悬挂指针。
    unique_ptr<T> ptr是一个可以保证只有一个unique_ptr指针指向一个对象,它的初始化方法只要有以下几种:
    1)unique_ptr<T> ptr(q); //q可以是一块新分配的内存或者普通指针。
    2)unique_ptr<T, D> ptr = ...; 和shared_ptr不同,unique_ptr需要指定自定义删除函数类型的指针。
    ptr.release(); //此时ptr不再指向之前所指向的对象,并返回之前所指向对象的指针,调用这个函数的时候,最好(务必)将函数的返回值存到某个指针,不然就内存泄漏了。
    shared_ptr和unique_ptr几个相同的函数:
    1)p; //用p作为条件判断指针p是否为空。
    2)*p; //取p所指向的对象引用。
    3) p->mem; //访问p所指对象的成员变量。
    4)p.get(); //返回所指对象的普通指针。
    weak_ptr顾名思义,弱指针,将一个shared_ptr绑定到weak_ptr不会影响shared_ptr所指向对象的引用数。他主要有以下几个方法:
    1)weak_ptr<T> w = sp; 以及 w = sp; //将一个share_ptr绑定到weak_ptr。
    2)weak_ptr<T> w(sp); //同上。
    3)reset(); //释放绑定的sp;
    4)use_count(); //所指对象的引用数。
    5)expired(); //判断use_count()是否为0。0返回true,否则返回false。
    6)lock(); //返回所绑定的一个shared_ptr,如果已经不存在,则返回一个空的shared_ptr。
85,在C++中,有些方法会被类默认定义,如果不想让这些方法被定义,可以重新声明这些函数,并在后面加 = delete;比如:ClassName(ClassName&) = delete; 这就意味着ClassName没有拷贝构造函数,不能够被拷贝,实现这一功能也可以将拷贝构造函数定义成private或者protected。
86,将拷贝构造函数定义成private的可以阻止该类对象的一切拷贝,如果试图调用该类对象的拷贝构造函数,可能发生两种错误:
    1)编译错误:当用户代码试图拷贝一个该类的对象,就会导致编译错误。
    2)连接错误:该类的成员函数或者友元函数(或者类)发生拷贝该类的拷贝时,就会导致链接错误。
87,运算符重载:C++中运算符重载时需要注意操作符的顺序,有以下几点需要注意:
    1)赋值运算符(=),下标运算符([]),访问运算符(()),以及成员访问操作符(->)的运算符重载函数必须定义为成员函数。
    2)复合赋值运算符(+=, -=)一般来说最好定义为成员函数,但不是必须的。
    3)一些运算符可能改变对象的状态,或者和给定类型相关的运算符(比如++, --, *)等,通常应该定义为成员函数。
    4)对称运算符(比如,算数运算符和关系运算符),通常需要定义成非成员函数。
88,输入失败通常有两点原因:1)读入错误类型的数据。2)遇到了EOF。
89,如果一个类需要重定义下标构造函数,那么它需要实现两个版本的下标构造函数:
    1)返回普通的对象引用。
    2)const类型的函数,并且返回const对象引用。     //用于const对象的调用。
90,重载前自增(自减)和后自增有以下区别:
    1)声明:前自增应该声明为: ClassName& operator++(); 后自增应该声明为:ClassName operator++(int);为了和前自增区别,后自增函数需要传入一个整数,这个整数是什么值都无所谓,但是必须传入,一般来说最好不要使用这个整数,所以通常将该整数参数定义成匿名的。与前自增(返回对象引用)不同,后自增返回一个新的对象。
    2)定义:
         前自增:
                 ClassName& operator++()
                 {
                        ++cur;
                        return *this;
                 }
        后自增:
                 ClassName operator++(int)
                 {
                        ClassName tmp = *this;
                        ++(*this);
                        return tmp;
                 }
91,成员访问操作符:有两个:*和->,可以定义如下:
    ClassName {
         public:
              string& operator*() const
              {
                   return (*p)[curr];
               }
              string& operator->() const
              {
                   return &(this->operator*();
               }
     }
92,函数访问操作符,用法如下:
    ClassName {
        type operator()(args) const {
             do something;
         }
     }
    ClassName(args);即可访问ClassName所重载的访问操作符。
    这种方法结合模版可以定义一系列的函数对象。比如plus<int> intAdd; int sum = intAdd(1, 2);
    其它一些库函数类(对象)有:less<Type>, greater<Type>等等,这两个函数类可以用作比较器,比如:sort(num.begin(), num.end(), greater<int>()); //从大到小排序
      sort(num.begin(), num.end(), less<int>()); //从小到大排序
93,lambda表达式是一个函数对象不是函数指针,普通定义的函数名可以当作函数指针使用,在使用中为了避免他们的差异,可以使用function对象,这个类是定义在function头文件里面,使用方法如下:
    function<Type> f = func;     //Type是函数原型,可以自己指定,func是一个可访问的对象(指针)。
    f(args);     //args是func形参的实例化,此时调用f,就相当于调用func(args);
    具体例子:
    function<int(int, int)> f1 = add; // add是函数指针
    function<int(int, int)> f2 = divide; // divide是一个函数对象类的一个对象
    function<int(int, int)> f3 = []  (int i, int j) {return i*j;} 
    cout << f1(4, 2) << endl;     //输出6
    cout << f2(4, 2) << endl;     //输出2
    cout << f3(4, 2) << endl;     //输出8
94,在C++中有的类型之间可以相互转换,但是有的时候并不希望他们之间相互转换,因此在C++11中,提供了一个新的关键字explicit,如果一个转换操作符被声明为explicit,那么这个转换操作只能显示地转换,不能隐式地转换,除了一个例外,这个例外就是在条件语句中,即使操作已经被声明为explicit,但还是可以隐式转换。具体例子如下:
    class SmallInt {
          public:
              explicit operator int() const {return val;}    
     }
    SmallInt si = 3;     //这不是隐式类型转换,是调用了构造函数,所以是对的。
    si + 3;     //错误,原因需要隐式转换成int才能进行运算。
    static_cast<int> (si) + 3;     //可行,因为显示转换了。
    一个常用的例子出现在输入的时候:
    while (cin >> value)      //这个例子中,条件语句会将cin隐式地转换成bool类型,但是不能在其它情况下隐式地转换。
95,静态变量(函数)也可以继承,并且可以通过类对象去访问,但是无论怎么集成,但是一个类(包括它的派生类)的一个静态实例只存在一个,但是是共享的。
96,在C++11中,也加入了final关键字,如果一个类被声明为final的,那么他就不能再派生出子类。
    比如: class Last final : Base {};     class Bad : Last {};     //就会导致编译错误。
97,初始化时调用构造函数,赋值时调用赋值重载函数,可能同样是等号,但是调用的函数不一样。
98,当用一个派生类的对象初始化或者赋值给一个基类的对象时,只有派生类中积累部分的属性能够被初始化和赋值,其它的属性都会被忽略(丢失)。派生类向基类的转换只限于指针和引用类型,派生类对象可以隐式地转向基类对象,但是会丢失信息;基类对象不能隐式地转向派生类对象。
99,多态必须依赖于指针和引用类型实现,通过对象本身调用方法实在编译期间就绑定了,不能实现多态。
100,派生类覆盖实现继承自基类的虚函数时,参数列表必须相同,并且除了一个例外,返回类型也必须相同,但是,如果返回自身引用或者指针时,派生类可以返回派生类指针和引用。如果虚函数有默认参数,那么基类和派生类对应的该默认参数都必须一致。
101,C++11中新加了关键字override,当一个派生类覆盖一个继承自基类的虚函数时,最好在声明的最后面添加override关键字(比如void f(int) const override;),这样如果基类并没有这个虚函数时就会报错。
102,在调用一个虚函数时,如果不想实现动态绑定,那么可以通过类的域运算符指定需要调用的函数,比如:ptr->Base::f();
103,包含有纯虚函数的类(包括继承虚函数并且没有重定义虚函数的类)叫做抽象类,抽象类不能被实例化,其它方面(包括构造函数)和普通类无差异,纯虚函数只需在函数声明的末尾添加=0; =0;只能出现在函数体内的虚函数声明。一般来说,无需为虚函数提供定义,但是也可以为其提供定义,但是必须在类的外部,并且函数原型要去掉=0; (如果纯虚函数定义成private的呢?)
104,一个类的友元不能访问基类的非public属性,但是可以该类的对象访问基类定义的属性。
105,一个类从基类继承属性以后可以调整这些属性的访问权限(可见度),比如:
    class Base {
        public: size_t size() const { return n; }
          protected: size_t n;
     }
    class Derived: private Base {
        public: using Base::size();
          protected: using Base::n;
     }    
    但是有个限制,只能调整当前类可访问属性的访问权限。
106,如果基类的函数不声明为虚函数,那么当派生类中有函数与它重名(只需函数名重名即可)是会将其覆盖。同样地,成员变量如果重名也会被覆盖。但还是可以通过域操作符访问基类与派生类函数同名的基类函数。
107,假如基类中有一个函数的多个重载版本,那么基类在覆盖实现的时候要么就覆盖所有版本要么就不覆盖该函数。但是覆盖一个函数的所有版本有的时候会比较麻烦,可以使用using关键字声明某些版本使用基类的。
108,如果一个类有派生类,那么它的析构函数应该定义为虚函数,否则当delete一个基类指针会引起undefined错误。只要基类析构函数声明为虚函数,那么它的所有派生类的析构函数也是虚函数。
109,如果基类中的默认构造函数,拷贝构造函数,拷贝赋值函数或者析构函数被声明为delete,或者不能被外部访问(private),那么派生类相关的函数也会被声明为delete,因为如果不声明为delete,在使用派生类创建对象时或者delete派生类对象时,会调用基类相关的函数,从而会导致错误。
110,如果基类构造函数有多个默认参数,那么这些默认参数不会被继承,此时派生类会对该构造函数继承多个版本,从左到右依次省略默认参数,有多少个默认参数就会继承出多少各相应的构造函数。如果派生类中定义了一个构造函数,参数列表(及默认参数),那么基类对应的构造函数就不会被继承。
111,模版定义方式:
    template <typename T0, typename T1, class T2, struct T3>
    define something;
    通常地,应该使用typename,它更通用。
112,模版的声明和定义都应该放到同一个头文件。
113,模版参数类型必须完全匹配。
114,模版的类型的声明顺序必须和使用顺序一致,从左到右,并且在使用时想省略某些类型参数,也只能尽可能省略右边的(实例化定义中类型出现的顺序)。
    比如:
    template <typename T1, typename T2, typename T3>
    T3 alternative_sum(T2, T1);
    auto val = alternative_sum<long long>(i, lng);      //此时只指定了函数的第二个,而第一个却省略了,所以错误。
    此时只能显示指定模版的参数类型:
    auto val = alternative<long long, int, long> (i, lng);
115,在模版方法中,可以用???来定义函数返回类型,这样可以动态地确定返回类型,比如:
    template <typename T)
    ??? &fcn(T beg, T end)
   {
          return *beg;
     }
    在C++11中,也可以通过trailing return来实现这一功能:
    template <typename T>
    auto fcn(T beg, T end) -> decltype(*beg)
    {
          return *beg;
     }
116,模版函数还可以定义可变个模版类型参数,比如:
    template <typename T, typename... Args>
    void foo(const T &t, const Args& ... rest);
    int i = 0; double d = 3.14; string s = "hello";
    foo(i, s, 42, d);
    foo(s, 42, "hi");
    foo(d, s);
    foo("hi");
    以上函数会自动绑定rest。
117,Template Specializations没有搞懂,需要继续研究。
118,在C++11中,新加入了tuple类型,tuple的初始化通常有三种:
    1)通过构造函数初始化,tuple<string, int, list<int>> someVal("constants", 42, {0, 1, 2});
    2)list初始化:tuple<int, int, int> threeD{1, 2, 3};
          由于tuple的构造函数被声明为explicit,所以必须像上面一样直接初始化,而不能:
          tuple<int, int, int> threeD = {1, 2, 3};
    3)make_tuple: auto item = make_tuple("constants", 3, 2.0);
    两个相同类型的tuple可以相互比较,从左到右比较大小。
    获取tuple的第i个元素可以通过:get<i>(tuple);
119,bitset也是一种非常有用的数据结构,有以下几种初始化方法:
    1)bitset<n> b;     //默认初始化为n位0
    2)bitset<n> b(u);  //定义一个n bit的变量b,并且用一个unsigned long long 类型的值u去初始化,如果u的位数大于n,那么用u的低n位初始化b;如果u的位数小于n,那么b的高位用0填充。
    3)bitset<n> b(s, pos, m, zero, one);     //定义一个n bit的变量b,用string s从pos位置开始的m位去初始化b,并且保证s中只有zero和one字符(用户指定),否则会抛出invalid_argument异常。这个构造函数的后四个参数都有默认值,pos的默认值是起始位置,m是string::npos,zero默认是'0', one默认是'1'。
    4)bitset<n> b(cp, pos, m, zero, one);和上面的构造函数很类似,只是cp是一个C-style字符串指针。
    bitset的只要操作有以下几个:
    b.any():返回是否有一个bit为1
    b.all():返回是否所有bit都是1
    b.none():返回是否所有bit都是0
    b.size():返回b的大小
    b.test(pos):返回b的pos是否为1
    b.set(pos, v),b.set():为某一位设置值v,v默认为true;如果不带参数,默认将b的所有bit都置位1
    b.flip(pos),b.flip():翻转某一位,或者全部
    b[pos]:取得b的第pos位的引用。
120,C++异常中,如果异常类型和继承相关,那么应该将异常参数定义成引用(便于实现多态)。
121,异常的匹配并不要求最佳,至匹配最早catch到的,不同类型的异常大部分情况是不允许的,典型的除了以下几种:
    1)nonconst转const。
    2)派生类转基类。
    3)数组转数组指针,函数转函数指针。
122,一个异常被捕捉到以后还可以再次抛出,只需一条空throw语句即可:throw;,空throw语句只能用于catch块内,或者被catch块调用的函数内。空throw不需要指定异常对象,会将捕捉到的异常对象继续抛出去。
123,想将所有异常都catch,可用:catch(...) {}
124,在构造函数中handle来自构造函数初始化器的唯一方式,是将构造函数写成一个try块。???
125,在C++中可以用namespace来处理明明冲突,可以在namespace外部定义该namespace的属性,但是必须通过域运算符指定所属namespace,比如:int NamespaceName::func();
    可以通过::运算符来指定全局namespace,比如:::member_name;
    在C++11中,还定义了inline namespace,如果一个namespace声明为inline,那么当将其包含到某个文件时,该namespace内的代码会被直接引入,比如:
    inline namespace FifthEd {}
    namespace FourthEd {}
    namespace cpluscplus_primer {
     #include "FifthEd.h"
    #include "FourthEd.h"
     }
    此时只需通过cplusplus_primer::即可访问到FifthEd namespace的成员,而访问FourthEd namespace的成员需要指定cpluscplus_primer::FourthEd::。
    namespace内成员定义是有顺序的,当前成员只能依赖它之前定义的成员。namespace的域搜索是从外到内,而成员确定域返回是从内到外。
    Overloading and Namespaces 和 Friend Declarations and Argument-Dependent Lookup不清楚。
126,在多重继承时,可能继承多个同名的成员,这是合法的,但是在使用的时候必须指定域,否则就ambiguous。当继承多个同名函数时,此时多个继承版本的同名函数参数列表必须一致。并且不能出现一个在private内,而另一个在public或者protected的情况。
    多重继承中,还可能继承多个相同的基类,比如:istream 和 ostream 都继承自 basic_ios,而iostream继承自istream和ostream,此时它会从istream以及ostream都继承basic_ios,此时如果使用一般的继承方法,必然会导致编译错误。virtual inheritance虚拟继承可以解决这个问题,通过virtual inheritance可以使一个类共享它的基类,被共享的基类成为虚拟基类。比如:
    class A: virtual public Base {}
    class B: virtual public Base {}
    class C: public A, public B {}
    此时C可以直接访问Base的成员,如果Base的某个成员被A或B覆盖,那么可以直接使用被覆盖以后的成员。但是如果A和B都覆盖了Base的某个成员,那么C就需要自定义一个覆盖。
    当C被创建时,一般来说需要显示初始化Base,而且必须首先初始化Base,尽管Base不是C的直接基类,如果不直接初始化,Base会调用默认构造函数,如果Base不存在默认构造函数,那么就会导致编译错误。
127,new 是为一个对象申请一块空间,new []是为一个数组分配一个空间,new函数的原型是:
    void *operator new(size_t);
    也可以自己重载该操作符,但是不能重载成这种形式:void *operator new(size_t, void*);因为这种形式是函数库预留的一种形式。
    当用new为某个变量或者数组分配空间时,首先计算需要分配多少空间来hold住变量或者数组,然后调用new(size_t);返回分配空间的void指针。简单的实现可以如下:
    void *operator new(size_t size) {
        if (void *mem = malloc(size)) return mem;
          else throw bad_alloc();
     }
    void operator delete(void *mem) noexcept { free(mem); }
    对象指针也可以直接调用析构函数,比如:string *sp = new string("a value"); sp->~string();此时指针指向的这块区域会清楚指针指向对象的内容,但是并不会释放内存,依然可以重用这块内存。所以通过直接调用析构函数会销毁指针指向的对象,但是不会释放内存。
128,如果要实现对象的相等比较,可以通过如下方式实现:
class Base {
    friend bool operator == (const Base &, const Base &);     
public:
    ..
protected:
    virtual bool equal(const Base&) const;
};
class Derived: public Base {
public:
    ..
protected:
    bool equal(const Base&) const;
}
bool operator == (const Base &A, const Base &B)
{
    return typeid(A) == typeid(B) && A.equal(B);          //typeid操作是用来获取对象的类型。
}
129,C++中,枚举(关键字enum)是一个整数常量的集合,通常可以定义如下:
    enum EnumName {a, b, c};          //可以为a,b,c分别指定值,默认第一个元素为0,默认后面一个元素比前一个元素大1。
    也可以定义无名枚举。
    以上定义方式是unscoped,在C++11标准中,还有scoped枚举,enum class ScopedEnum {a, b, c};对于scoped枚举只能通过域运算符去使用。
    一个枚举对象只能被初始化为枚举整数集合内的一个值,比如enum color {red, yellow, green}; color v = red;
    还可以指定枚举元素的整数类型,比如:
    enum intValues: unsigned long long { charTyp = 255, shortTyp = 65525, intTyp = 65535}
130,指向类成员的指针是一个可以指向一个类的非静态成员的指针。指向类成员的指针可以分两类:
    1)指向数据成员的指针:
          比如:
    const string Screen::*pdata;     //定义一个指针pdata,pdata可以指向一个Screen对象的string数据成员变量。
    pdata = &Screen::contents;     //将指针pdata和Screen类的一个string变量绑定。
    或者 auto pdata = &Screen::contents;
    Screen myScreen, *pScreen = &myScreen;
    此时就可以通过Screen的对象或者指针去访问指针:auto s = myScreen.*pdata; s = pScreen->*pdata;
    以上的使用方法,当一个数据成员是private,在类外部以及友元外部使用将会导致错误。可以通过返回指向数据成员的指针的方式来实现,比如:
    class Screen {
     public:
          static const string Screen::*data() { return &Screen::contents;}
     }
    const string Screen::*pdata = Screen::data();
    auto s = myScreen.*pdata;          //此时无论pdata指向的数据是private还是public,都可以合法访问。
    2)指向成员函数的指针:
    比如:
    char (Screen::*pmf) (Screen::pos, Screen::pos) const;
    pmf = &Screen::get;     / auto pmf = &Screen::get;
    在定义pmf的时候,括号不能省略,省略以后编程:char Screen::*pmf(Screen::pos, Screen::pos) const;是指声明一个返回指向Screen中char类型的数据成员的指针,但是Screen中没有const的静态函数,所以导致编译错误。
    char c1 = (pScreen->*pmf)();
    char c2 = (myScreen.*pmf2)(0, 0);
131,在C++中,支持嵌套类,但是嵌套类和外部类几乎没有关系,嵌套类的对象并不拥有外部类的成员,同样地,外部类的对象也不拥有嵌套类的成员。嵌套类并没有特殊的权限访问外部类,同样地,外部类也没有特殊的权限访问嵌套类。嵌套类的类名只在它的外部类可见。
    当嵌套类定义在外部类的外面,需要用多重域访问运算符指定,比如:
    class A {public: class B;}; class A::B  {define something;}; 
132,联合(union)是一种节约空间的类,在一个union中,可以有多个数据成员,但是同时只能有一个数据成员有值,其他成员都是undefined的,不占内存。union中不能有引用数据成员。和struct一样,union中的数据成员默认为public的。也可以定义匿名union。比如:union {char cval; int ival; double dval; };
     cval = 'c'; //给匿名union赋一个新值
    ival = 42; //此时这个匿名union持有42这个值
    匿名union不能有private和protected成员,并且不能定义成员函数。
133,局部类,局部类的定义必须在类的内部,并且出了它的定义域,就失效。
1 0
原创粉丝点击