当前位置:   article > 正文

C ++ primer_c++ primer

c++ primer

C++基础

2 变量和基本类型

c++ 定义了一套包括算术类型和空类型(void)在内的基本数据类型。空类型只用于一些特殊场合,如函数不返回任何值时。

算术类型分类

  1. 算术类型分为整型和浮点型。字符和布尔都算作整型。
    C++规定了尺寸的最小值, char:1 short int:2 int:4 long int 4; float:4; double:8;long long int 8. bool类型最小尺寸未定义。
  2. 带符号和无符号类型。
    字符类型被分为了三种,char、signed char、unsigned char。 char的类型和编译器有关,有时候是无符号,有时候是有符号。建议使用char的时候指定有符号,无符号。不要用char。
  3. 使用规则:
    1. 明确知道结果不可能为负数时,使用无符号数。
    2. 执行浮点数运算选用double.float精度不够而且与double计算代价相差无几。

类型转换

  1. 浮点数->整数, 取小数点前面的那个 理解内核 double a = 2177.20,a实际上可能为2177.1999,尤其是在qt里面使用double_Spinbox,需要进行精度补充。
  2. 整数->浮点数,小数部分设置为0

浮点数:

浮点数表示方法:二进制表示
原文链接:https://blog.csdn.net/u013416923/article/details/121711879

问题:

表示不精确,是实数的近似值,比较时需要使用精度。如fabs(x)<1e-6
原因:十进制小数转为二进制小数时,有精度损失。
链接:https://blog.csdn.net/weixin_70298631/article/details/125364840

有符号数与无符号数转换。

同类型,有符号与无符号,直接内存拷贝。有无符号转换:2的n次方
**有符号数转换为无符号数时,负数转换为大的正数,相当于在原值上加上2的n次方,而正数保持不变。
**无符号数转换为有符号数时,对于小的数将保持原值,对于大的数将转换为负数,相当于原值减去2的n次方。

大类型转换为小类型,截断:大小端。

隐式转换拓展:

  1. 算术运算需要转换
    1. 整型提升。整型提升负责把小整数转换成较大的整数类型。所谓小整型一般就是一下几种:char<short≤int≤unsigned≤long≤unsigned
      long < float < double
      只要它们所有可能的值都能储存在int里,它们就会提升称为int类型;否则,提升成为unsigned int类型。
    2. 无符号类型的运算对象
      a. 同类型(有/无符号),但是大小不一致。小类型转化为大类型形如: 如int 和 long 运算,转化为 long 和 long 运算;
      b. 有符号和无符号一起,但是类型大小相同,则有符号转换为无符号的。如:int 和 unsigned int 运算,转化为 unsigned int 和 unsgined int 运算。
      c. 有符号和无符号一起,且带符号的类型比无符号的类型大。如:unsigned int 和 long 运算。
      这种情况最为复杂,其转化结果依赖于机器。如果无符号类型的所有值都能存在于带符号类型中(也即带符号的类型所占空间大一些),则无符号类型的运算对象转化成带符号类型。
      上述情款转化为:long 和 long 运算
      如果不能(即带符号的类型和无符号的类型所占的空间一样大),带符号类型的运算对象转化成无符号类型。
      上述情款转化为:unsigned int 和 unsigned int 运算。
unsigned int a = 20;
signed int b = -130;
比大小:结果是b > a
  • 1
  • 2
  • 3
2. 赋值需要转换
      右边的转换为左边的类型。
      若右边的数据类型的长度大于左边,则要进行截断。
      右边长度小于左边,扩展。 有符号变为更长的有符号,值不变。
3. 输出转换
	printf会把char、short自动转换成int。  float自动转换成double

强制类型转换
		(类型名)(表达式)。得到中间变量,原变量不变
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

计算机中数据表示方法:

计算机中数据以补码形式存储。

  1. 计算器被设计成只能计算加法,不能计算减法,符号位也参与计算
  2. 用反码,有2个0
  3. 用补码表示可以消除两个0的编码问题 1000 0000表示-128是人为规定的 一个字节 -128-127 表示: 正整数,都一样 负整数,负数的反码是符号位不变其余原码逐位取反。
    补码,符号位参与,补码+1.
  1. 字面值常量:
    形如42的值叫做字面值常量。每个字面值常量都对于一种数据类型。
    整型:十进制:20 八进制:024 16进制:0X14
    浮点型:3.14 3.14E0 0. 0e0 .001
    字符:‘a’ ‘0’----48 ‘\0’ ---- 0 ‘a’----97 ‘A’-----65
    字符串:“hello world” 末尾加一个空字符’\0’
    转义序列:简单来说就是在一些字符前加 “\” 使它具有其他意义
> \\ \ 反斜线 
>  \"  "   双引号 
> \' ' 单引号
>  \ooo   八进制数 ,可以跟1-38进制数
>  \xhh   十六进制数,可以跟1~216进制数
  • 1
  • 2
  • 3
  • 4
  • 5

添加前后缀可指定字面值的类型。
u8"hi" utf8的字面值
42ULL 42无符号长长整型。
布尔字面值:true false
指针字面值:nullptr
c中是这样定义NULL: #define NULL ((void *)0)

字符常量,用单引号括起来的单个普通字符或转义字符.

变量

  1. 定义:

变量提供一个具名的、可供程序操作的存储空间。C++中的每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。对C++程序员来说,变量和对象一般可以互换使用。

  1. 初始化和赋值:

初始化不是赋值,初始化的含义是创建变量的同时赋予其一个值,而赋值的含义是吧对象当前的值擦除,而以一个新的值来代替。

  1. 初始化方式
  1. 列表初始化,用{}来初始化变量。
  2. 默认初始化:> 如果定义时没有初始化,则是默认初始化,默认值由变量类型以及定义变量的位置决定。 定义在任何结构体外的变量被初始化为0。例外: 定义在函数体内的内置类型变量不被初始化,值未知。
    没有被初始化,错误不可知。
  3. C++ 11新标准,可以为类内的数据成员提供一个类内初始值。花括号或=,不可圆括号。
  1. 变量声明和定义。

程序分为多个文件,为了在文件间共享代码,需要支持分离式编译。声明使得名字为程序所知,一个文件如果想要使用别处定义的名字则必须包含对那个名字的声明。而定义负责创建与名字关联的实体。

声明:多个cpp文件,用同一个变量。
extern int i; 声明 规定类型和名字,
extern int i = 0; 定义 分配空间和初值。

1)类声明:class 类名;
a.前向声明:可以仅仅声明类而暂时不定义它,是个不完全类型。可以用指针或引用。声明(但没有定义)的函数的参数或返回类型。类允许包含指向它自身类型的引用或指针。
2)函数声明:
与定义的区别是没有函数体,用分号替代。
可以不包含形参名字,但为了可读性,最好还是写上。

复合类型

  1. 引用

一般指左值引用,为对象起的别名。 右值引用:c++
11新特性,用&&来获得,只能绑定即将销毁的对象。常数、表达式、函数返回值。主要是为了提高效率。相当于延长了生命周期。 疑惑:return
做了什么? https://blog.csdn.net/jeff_/article/details/48915759
左值:变量等,等号左边,长久保存。 右值:临时值,等号右边,短暂存在。 右值引用可以减少拷贝的次数,提高效率。 std::move
将右值引用绑定到左值上。我们可以销毁一个移后源对象,也可以赋予新值,但不能使用一个移后源对象的值。
对右值进行浅拷贝,右值对象中的指针直接赋值给新的对象,然后将右值对象的指针赋NULL.

  1. 指针:

一个特殊的变量,存储地址 遇到指针,都应该问三个问题:指针的类型(char *),指针所指向的类型,指针的值
nullptr指针作用:1、解决函数调用歧义 2、构造函数重载 2个指针比较 == 指针的值比较。

复合类型的声明:

从右往左读,离变量名最近的符号对变量类型有最直接的影响。 指针数组 : *p[n] 数组指针 :(*p)[n]
数组指针:可以指向二维数组,此时+1 表示到了下一行
二级指针:指向指针数组
sizeof 数组: 数组的大小 指针:4或8字节

const 限定符

  1. const变量和普通的变量一样,特殊的是初始化后值不能改变。当以编译时初始化的方式定义一个const变量时,如const int bufSize = 512;
  2. 编译的时候,会把所有用到该变量的地方替换对应的值。默认仅在当前文件里有效,当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
  3. 要多文件共享,需要定义和声明的时候加extern。
  4. const引用:通常情况下,引用只能绑定到对象上,而不能与字面值或某个表达式的结果绑定在一起。但const引用绑定到另一个类型时,会构造一个临时变量,而绑定到临时变量上。

const 指针:int * const p; 从右往左读,就近原则。
指向常量的指针: const int *p; 指针不能修改里面的值。

顶层const

  1. 顶层const表示指针本身是个常量,底层const表示指针所指向的对象是个常量。
  2. 顶层const可以表示任意对象是常量,底层const与指针和引用等有关。
  3. 拷贝操作时,底层const的限制不能忽视。拷入和拷出的对象必须有相同的底层const资格,或者2个数据类型必须能够转换。非常量可以转换为常量,反过来不行。
  4. 顶层const不影响拷贝操作。

处理类型

类型别名:

typedef; 定义类型别名。
与const结合会有意想不到的效果。
比如:typedef char * pstring. const pstring cstr; cstr是一个指向char的常量指针。

换种理解方式

:typedef int integer;

  1. 将typedef去掉,那就是个变量的定义,这儿即定义了一个int型的变量integer,考虑这个integer是什么类型的,那么这个typedef语句就是将integer定义为这个类型的。
  2. 将typedef int (*func_pointer)(const&,
    int);中的typedef去掉,就成了一个函数指针定义,即func_pointer被定义为函数指针类型变量,那么原来的typedef即将func_pointer定义为函数指针类型.

auto:

让编译器通过初始值来推断变量的类型(发生在编译期)。 特殊:当引用作为初始值时,真正参与的是引用对象的值。

  1. 声明为auto(不是auto&)的变量,会忽视掉表达式顶层的const。
  2. 声明为auto&的变量,保持表达式的顶层const或volatile属性
  3. 若希望推导的是顶层const,加上const,即const auto。

自定义数据结构

  1. 数据结构就是把一列相关的元素组合起来,然后使用它们的策略和方法。
  2. 定义一个类,也就是定义了一个类型。类体}后面要加上分号。命名空间不需要加。

命名空间使用:using声明。

  1. 使用using声明后,每次调用时就不需要再加前缀了。 如using std::cin;
  2. 头文件不应该using声明,可能会产生难以预料的名字冲突。

头文件:预处理变量无视作用域。 ifndef,一般以类名大写表示头文件 _H.

#define 和 const区别:
1. define是在编译的预处理阶段起作用,而const是在编译阶段作用。
2. const有类型检查,可以调试。

结构体
1. 对齐字节需要知道的3个概念:自身对齐值、指定对齐值、有效对齐值。
2. 自身对齐值:数据类型本身的对齐值,例如char类型的自身对齐值是1,short类型是2;
指定对齐值:编译器或程序员指定的对齐值,32位单片机的指定对齐值默认是4;#progma pack(n) 指定对齐字节值
有效对齐值:自身对齐值和指定对齐值中较小的那个。

对齐有两个规则:
1、存放成员的起始地址必须是该成员有效对齐值的整数倍。
2、结构体的有效对齐值是其最大数据成员的自身对齐值;

  1. 共用体(联合体) union 取最大值,所有成员相对于基址地址的偏移量为0。可以用来判断大小端。大端:高字节放在低地址。小端,低字节放在低地址。高数据截断类似。网络是大端结构,大多数主机是小端结构。
  2. 枚举 缺省值为0,1,2…;若赋值,自动加1
    赋值时,需要赋枚举里面的值,不能直接1,2

3 字符串、向量和数组

标准库类型 string

std::string (也就是C++中的string):标准并没有规定字符串必须以\0字符结尾。编译器在实现时既可以在结尾加上\0,也可以不加,这个是由编译器决定的,但是当通过c_str()或者data()转换得到的const char *时,会发现最后一个字符一定是\0。

string 格式
因为string是一个类,它的长度信息已经封装到类的私有变量里面了。

  1. 直接初始化和拷贝初始化。使用等号的是拷贝初始化,否则是直接初始化。

  2. 读写string 对象。 cin >> str; cin 时,string会自动忽略开头的空白(空格,换行,制表符),遇到下一个空白停止。string对象返回运算符左侧的运算对象作为结果,因此可以连续输入。
    cin >> str1 >> str2;
    while ( cin >> str) 遇到文件结束符或非法输入结束。

  3. string对象比较。 相同,短的< 长的。

  4. string对象相加,每个加法运算符+的两侧的运算对象至少有一个是string。操作符重载:
    https://blog.csdn.net/weixin_61857742/article/details/126010673

  5. 下标访问,[]返回的是该位置上字符的引用

  6. size和Length是不包含空字符的。

截取字符串
.substr(起始位置,要截取的长度);

搜索
返回内容所在的位置,当返回值为std::string::npos时,表示未找到。
.find(要搜索的内容,开始搜索的位置);
.find(要搜索的内容,开始搜索的位置,要纳入搜索的长度);
rfind()是从字符串的末端开始查找,返回的是正向的下标

插入字符串
.insert(要插入的位置,要插入的字符串);
.instert(要插入的位置,要插入的字符个数,要插入的字符);

替换字符串
.replace(要替换的内容起始位置,要替换的长度,“替换后的内容”);

删除字符串
.erase(要删除的起始位置,要删除的起始长度);
.erase(); 删除字符串所有内容;
.clear(); 删除字符串所有内容。

字符串和数字的相互转换
在这里插入图片描述
std::string 字符编码
原文链接:https://blog.csdn.net/wanggao_1990/article/details/113973730

标准库类型vector

vector存放的是某种给定类型对象的可变长序列。

初始化

  1. 列表初始化 {}
  2. 值初始化 () vector vec(10, -1); 生成10个-1的元素,如果没写-1,则为0. string则默认初始化。 二维数组:vector<vector> vec(m, vector(n,0));
    m*n的二维数组,所有元素都为0

插入

  1. vec.insert(vec.begin()+i,a);在下标为i的元素前面插入a;
  2. 插入时写emplace_back,而不是push_back; push_back()构造2次,先构造临时对象,在插入的时候拷贝。 emplace_back只构造一次,在插入的时候直接构造。
    若push_back/emplace_back传递的参数为已被构造的对象,则没有差别。

容量

1.vec.size() 当前容器所存储的元素个数
2.vec.capacity() 容器在分配新的存储空间之前能存储的元素总数
3. resize()操作:创建指定数量的的元素并指定vector的存储空间; reserve()操作:保留,capacity;
区别:

  1. 当n大于当前元素个数,resize和reserve都会capacity。根据分配策略,可能会有更大的一块。resize未指定初始化参数,按类型默认初始化,添加元素。而reserve不会添加元素。
  2. n < 当前元素个数,resize删除多余的,capacity不改变。而reserve什么也不做。

内存扩充策略

满了的时候,成倍扩充,然后拷贝原有数据到新内存,释放原内存。

内存泄漏:

clear()和erase(),resize()只改变size,不改变capacity。解决:ivec.swap(vector(ivec));
定义一个临时变量,交换内容

查找

find(vec.begin(), vec.end(), i)  != vec.end();
  • 1

删除元素

  1. vec.erease(vec.begin(), vec.begin()+1); 删除第一个元素 左闭右开
  2. pop_back() 删除最后一个元素,尽量不要从中间删除。

测试网站:https://cpp.sh/

迭代器

  1. 所有标准库容器都可以使用迭代器,只有少数几种支持下标运算符。
  2. v.end();表示尾元素的下一位置,当容器为空时,begin==end
  3. *iterator 返回所指元素的引用
  4. iterator->mem 等价于 (*iterator).mem
  5. 迭代器类型 iterator(读写) const_iterator(只读) cbegin返回const_iterator.
  6. erase删除容器后,返回下一个迭代器。
    迭代器使用:可以理解为封装了指针
    容器::iterator,如map<char,string>::iterator

迭代器失效

  1. 会引起其底层空间改变的操作,都有可能是迭代器失效 ,
    比如: resize 、 reserve 、 insert 、 assign 、push_back等。
  2. 指定位置元素的删除操作 - -erase
    erase 删除 pos 位置元素后, pos 位置之后的元素会往前搬移,没有导致底层空间的改变,
    理论上讲迭代器不应该会失效,但是:如果pos 刚好是最后一个元素,
    删完之后 pos 刚好是 end 的位置,而 end 位置是没有元素的,那么pos 就失效了。
    因此删除 vector 中任意位置上元素时, vs 就认为该位置迭代器失效了。

迭代器失效

  1. 野指针 扩容
  2. erease或insert后,迭代器意义改变

数组

初始化

  1. 数组未初始化的时候,为默认初始化
  2. 部分初始化的时候,类似vector值初始化,其他值采用默认值
  3. 复杂的数组声明解析,从数组的名字按照从内向外的顺序读,先右后左。 多维数组,指的是数组的数组,按照名字从内到外的顺序阅读。

4. 表达式

表达式由一个或多个运算对象组成,对表达式求值将得到一个结果。字面值和变量是最简单的表达式,结果是他们的值。

左值和右值

右值的时候,用的是对象的内容;左值的时候,用的是对象的身份。

算术运算符满足左结合律,如果优先级相同,按照从左往右的顺序。

括号无视优先级与结合律。

求值顺序

运算对象的求值顺序和优先级和结合律无关。优先级只是规定了运算对象的组合方式。 int i = f1() * f2();
未说明f1还是f2先计算 cout << i << ++ i << endl; 未定义

算术运算符—取模:

早期允许m%n的符号匹配n的符号,并且商向负无穷一侧取整。C++新标准已经禁用,除了-m导致溢出的特殊情况,其他时候:(-m)/n
,m/(-n) == -(m/n); m%(-n)=m%n;(-m)%n=-(m%n). 商一律向0取整。

逻辑和关系运算符

与和或都是先算左边再算右边,如果左边可以确定整个的值,则不会计算右边的(短路求值)。
关系运算符的求值结果是布尔值,if (i < j < k)

赋值运算符

赋值运算满足右结合律,返回的是左侧运算对象。 ival = ijal = 0; 都为0

递增和递减运算符

前置版本,返回改变后的对象。 后置版本,返回改变前的对象。 (会生成一个副本,减低效率 j++,一般用前置版本)
*p++表示先取值,然后指针自增1
*beg = toupper(*beg++); 未定义错误,不知道先算左边的还是右边的。

成员访问运算符

p->mem 等价于(*p).mem 加括号是因为解引用优先级低于.

条件运算符

cond ? exp1 : exp2 首先求cond的值,如果为真对exp1求值并返回其值,否则对exp2求值并返回其值。
在输出表达式中运用条件运算符时,需要加括号,优先级较低。

逗号运算符

常用于for循环中。

强制类型转换
有4种类型。

  1. static_cast
    用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用static_cast,但它不能用于两个不相关的类型进行转换。即: static_cast 相近类型之间的类型。
  2. reinterpret_cast,即重新解释
    reinterpret_cast操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换为另一种不同的类型,即: reinterpret_cast 不相近类型之间的类型
  3. const_cast
    最常用的用途就是删除变量的const属性,方便赋值
  4. dynamic_cast 动态转换
    dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)。

向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则)
向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的),用static_cast不安全。

链接:
https://blog.csdn.net/zhang_si_hang/article/details/127239523

5.语句

空语句

;加了空语句需要加注释

条件语句

  1. 多层嵌套,可以提逻辑,减少嵌套层数
  2. else与离他最近的未匹配的if匹配

switch语句

  1. switch对表达式求值,然后值转变为整型
  2. case标签必须是整型常量表达式
  3. 如果表达式和某个case匹配,直到switch结尾或遇到break结束。接着执行switch之后的语句。
  4. 一般加default,表示我们考虑到了这个情况。
  5. switch内部定义的变量,如果没有初始化,其他分支可以用。初始化了其他分支不可以用。

迭代语句

  1. while语句,定义在条件部分和循环体内的变量,每次迭代都经历创建和销毁的过程。
  2. 传统for语句。 for (init;condition;expression)
  3. 范围for语句。
    for(declaration: expression).
    expression必须是一个序列。 declaration定义一个变量,每次迭代都重新定义变量,并将其初始化序列中的下一个值。
    对范围for语句,不能增加vector对象的元素。因为for(auto r : v) 等价于 for(auto beg =
    v.begin(), end = v.end(); beg != end; ++beg)。

跳转语句:

  1. break语句,终止离最近的while for do while switch语句,并执行其之后的第一条语句。
  2. continue,终止最里面的的循环中的当前迭代,并立即开始下一次迭代。对于传统for,继续执行for语句头的expression;对于范围for,用序列中的下一个元素初始化循环控制变量。
    只有switch嵌套在迭代语句里,才能用continue

6.函数

函数是一个命名了的代码块,通过调用函数执行对应的代码。函数可以重载,同一个名字可以对应几个不同的函数。

参数传递:

  1. 每次调用函数时会创建它的形参,并用传入的实参对形参进行初始化,形参的初始化和变量的初始化一样
  2. 使用引用可以避免拷贝,提高效率,最好用常量引用。 尽量使用常量引用1. 不使用常量引用会误导读者,函数可以改变引用的值 2. 函数内调用另一个函数,无法直接使用。

数组形参:

  1. int * int[] int[10] 都等价的,10只是表示期望,实际不影响
  2. 二维: int(*p)[10]; int p[][10] 第二个不可省略,一定要相等。

main命令行参数:

prog -d -o -ofile daa0 argc = 5, argv[0]表示程序的名字,argv[1]表示实参。

返回值

  1. void函数,使用return; 末尾会自动执行return;
  2. 有返回值的函数,要写return。main函数会自动插入return 0(如果没有写)。
  3. 返回一个值的方式和初始化一个变量或形参的方式完全一样,返回的值用于初始化调用点的一个临时变量。
  4. 不要返回局部对象的引用或指针,存储空间被释放,指向为无效的内存区域。

函数重载:

函数名相同,形参列表不同。返回值不同不构成重载。 编译器优化,传递一个非常量对象的指针时,编译器优先选用非常量函数。。
如果在内层作用域声明名字,将隐藏外层作用域中声明的同名实体,在不同作用域内无法重载函数名。变量名也可隐藏函数名。

默认实参:合理设计顺序,默认值的形参出现在后面。一般出现在声明中。
string func(int, int, char = ‘c’);
调用时,省略会使用默认实参。

内联函数:可避免函数调用的开销。
是对编译器发出的一个请求,编译器可以忽略请求。
适用于规模较小,流程直接,频繁调用的函数。一个75行的不太可能内联展开。

constexpr函数:
constexpr表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。

调试帮助:
assert预处理宏,当表达式结果为假时,输出信息并终止程序的执行。为真时,什么也不做。

assert(pFunc != nullptr);
  • 1
NDEBUG预处理变量,assert行为依赖了这个变量,如果定义了这个,则assert什么也不做。可以使用这个变量编写自己的条件调试代码。
C++编译器定义:
_ _ func _ _当前函数的名字,const char 数组
C++预处理器定义:
_ _ FILE_ _ 存放文件名
_ _ LINE_ _ 存放当前行号
_ _ TIME_ _ 存放文件编译时间
_ _ DATE_ _ 存放文件编译日期
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

函数匹配:
1. 选定本次调用的重载函数集,集中的函数称为候选函数。需要与调用函数同名,声明在调用点可见。
2. 根据实参,选出可行函数。需要形参数量与实参数量相同,实参与形参类型相同,或者能转换为形参类型。
3. 从可行函数中寻找最佳匹配。
基本思想:实参类型与形参类型越接近,匹配越好。
含有多个形参的函数匹配。
1. 该函数每个实参的匹配性都不劣于其他可行函数需要的匹配
2. 至少有一个实参的匹配优于其他可行函数的匹配。
如果检查了所有实参之后没有一个函数可以脱颖而出,则调用错误,编译器会报二义性错误。

实参类型转换等级
等级1:精确匹配:

1)实参和形参类型相同。
2)实参从数组类型或函数类型转换为对于的指针类型
3)向实参添加/删除顶层const.

等级2:通过const转换实现的匹配

1)将指向非常量类型的指针或引用转换成对应的常量类型的指针或引用。
2)非常量对象实参优先使用非常量形参函数。

等级3:通过类型提升实现的匹配

整型提升负责把小整数类型转换为较大的整数类型。如char<short≤int≤unsigned≤long≤unsigned

等级4:通过算术类型转换或指针转换实现的匹配

1)算术转换是把一种算术类型转换成另一种算术类型。
2)指针转换:0或nullptr能转换成任意指针类型。
3)指向任意非常量的指针能转换成void*.指向任意对象的指针能转换成const void *;
4)继承关系间指针的转换。

等级5:通过类类型转换实现的匹配。

示例
ff(int); ff(short);
ff(‘c’) 直接调用ff(int);
算术类型:所有算术类型转换的级别都一样。如从int转换为unsigned int并不比向double的转换级别高。
ff(long); ff(float);
ff(3.14); 错误,二义性调用。

函数指针
指向函数,函数的类型与返回类型和形参类型共同决定,与函数名无关。
返回指向函数的指针
using f = int(int*,int); f是函数类型,不是指针
using pf = int (*p)(int *, int) pf是指针类型。
返回类型不会自动地转换为指针,需要我们显式的转换。
pf f1(int);正确
f f1(int); 错误,f是函数类型
f * f1(int); 正确,函数指针。

7.类

内联函数:

定义在类内部的函数是隐式的内联函数。
宏存在的问题:

  1. 书写复杂
  2. 不支持调试
  3. 没有类型安全的检查

使用函数:会有入栈出栈的开销,降低效率

内联函数: 编译期间每一个形参都会被对应的实参取代,然后把函数体替换。 如果内联函数生效,断点调试会不生效,没有去调用函数。

使用:

  1. 在类里定义的函数被默认为是内联函数。
  2. 类中声明时加inline关键字,类外定义。

this指针

成员函数含有this指针,this是一个指针常量,顶层const,无法改变指向。
1)const成员函数,const关键字隐式修改this指针的类型。 由data * const p 变为 const data *
const p; 2)const成员函数无法改变对象的内容 3)常量对象,以及常量对象的引用和指针都只能调用常量成员函数。
4)const成员函数的定义也要在后面加const

深入: 常量指针:本质上是一个指针,常量表示指针指向的内容,说明该指针指向一个“常量”。
指针常量:本质上一个常量,指针用来说明常量的类型,表示该常量是一个指针类型的常量。指针无法更改。

记忆方式:

const(*号)左边放,我是指针变量指向常量;
const(*号)右边放,我是指针常量指向变量;
const(*号)两边放,我是指针常量指向常量;

构造函数:

1)构造函数负责类的初始化 2)构造函数没有返回值 3)构造函数不能声明为const。
当创建一个const对象时,直到构造函数完成初始化后,对象才获得const属性。

默认构造函数
1)默认构造函数就是在调用时不需要显示地传入实参的构造函数。无参数的”和“带缺省参数的都是,不要同时出现。一般情况下,默认构造函数是一个空的函数体。
2)如果类没有显示的定义构造函数,则编译器会隐式的定义一个默认构造函数,称为“合成的默认构造函数”。
a.如果存在类内的初始值,则用它来初始化
b.否则,默认初始化该成员。内置类型在函数体内未知值。如果类的对象没有显式地初始化,则其值由类确定。

某些类不能依赖于合成的默认构造函数

  1. 编译器只有在发现类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数。
  2. 含有内置类型或复合类型成员的类如果没有默认值,采用的默认初始化,值未定义。
  3. 编译器无法为某些类合成默认的构造函数。如果类中包含一个类类型没有默认的构造函数,则无法初始化该成员。

想要默认函数等同于合成的默认构造函数。
sales_data() = defaultl;

1.减轻程序员的编程工作量;
2.获得编译器自动生成的默认特殊成员函数的高的代码执行效率

初始化列表

  1. 如果没有在构造函数的初始化列表中显示地初始化成员,则该成员将在构造函数体之前执行默认初始化。
  2. 如果成员是const、引用或者属于某种未提供默认构造函数的类类型,则必须通过构造函数初始化列表为这些成员提供初值。
  3. 建议使用构造函数初始值:1.底层效率问题 2.一些数据成员必须初始化,可以避免某些意想不到的编译错误。
  4. 成员初始化顺序与在类内定义的出现顺序一致,与初始化列表的顺序无关。

sales_data obj(); //声明了一个函数而非对象

C++ primer(第5版)中写道:类内初始值的提供必需以=或者花括号{}的形式。不能用园括号()。

关于这一点,可以参考网上的一个说法,如下: 由于无法避免下面的这种情况,这相当于对于int
z(int);函数的声明,所以C++把用圆括号进行类内初始值定义为非法。

只接受一个实参的隐式转换机制:

  1. 只允许一步类类型转换。如item.combine(“sadfa”);无法将char *转换为string,然后再转为sales_data.
  2. 可以用关键字explicit,只对一个实参的构造函数有效,不支持隐式构造。只能在类内声明的时候使用,类外定义的时候不能重复。

class默认访问权限是私有,struct默认访问权限是公有。

友元:

类可以允许其他类或者函数访问它的非公有成员。友元,增加一条以friend开头的函数声明。 一般最好在类定义开始结束前的位置集中声明友元。
友元的声明仅仅指定了访问权限,而非一个通常意义上的函数声明。如果希望类能够调用函数,则要再加个函数声明。
友元类:友元类的成员函数可以访问包括非公有成员在内的所有成员。 友元关系不具备传递性。 每个类负责控制自己的友元类或友元函数。

类的其他特性:

可变数据成员:变量声明成mutable,任何时刻都可以更改它,即便是在const成员函数里。

类的声明:

可以只声明类而暂时不定义它,称为前向声明。称为不完全类型,可以用于:定义指向这种类型的指针或引用,也可以声明(不可以定义)以不完全类型作为参数或者返回类型的函数。
定义一个函数,编译器就会为函数的形参和返回值预先留出合适的内存空间。

类的作用域:

一个类就是一个作用域,在类外定义成员函数时必须同时提供类名和函数名。在类的外部,成员的名字被隐藏起来了。
一旦遇到类名,定义的剩余部分就在类的作用域之内的。这里指参数列表和函数体。 函数的返回类型在函数名之前,所以要加作用域。

名字查找和类的作用域:

内层作用域可以重新定义外层作用域的名字。但是如果在类中,成员使用了外层作用域的某个名字,而该名字代表了一种类型,则类不能再重新定义该名字。
成员函数使用的名字查找顺序:

  1. 在成员函数内部查找声明
  2. 在类内查找
  3. 在类外查找。 可以隐藏同名变量。 想要使用外部的,如::height;或类内的:this->height;

类的静态成员:

  1. 与类关联而不是与类的对象关联。
  2. 静态成员函数不能被声明为const,不包含this指针。
  3. static关键字只出现在类内部的声明语句中,无法出现在类外部。
  4. double Account::interestRate = initRate(); 虽然initRate是私有的,也能这样用。从类名开始,这条语句剩余的部分就都位于类的作用域之内了。
  5. 静态成员可以是不完全类型。如在class Bar里面定义static Bar mem1;

C++标准库

8. IO库

C++语言不直接处理输入输出,而是通过一族定义在标准库中的类型来处理IO。这些类型支持从设备读取数据、向设备写入数据的IO操作,设备可以是文件、控制台窗口等。还有一些类型允许内存IO,即从string读取数据,向string写入数据。

之前已经使用过的IO库设施小结:

istream输入流类型,提供输入操作
ostream输出流类型,提供输出操作
cin一个istream对象,从标准输入读取数据
cout一个ostream对象,向标准输出写入数据
cerr一个ostream对象,通常用于输出程序错误信息,写入到标准错误
>>用来从一个istream对象读取输入数据
<<用来向一个ostream对象写入输出数据
getline函数从一个给定的istream对象读取一行数据,存入一个给定的string对象中

8.1 IO类

目前为止,我们使用的IO类型和对象都是操纵char数据的,标准库还提供了支持宽字符读写的操作。
在这里插入图片描述

IO对象无拷贝或赋值

注意:不能拷贝IO对象,因此也不能将形参或返回类型设置为流类型。进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。

一个流一旦发生错误,其上的后续的IO操作都会发生失败。在使用流之前,检查它是否处于良好状态。确定一个流对象的状态的最简单的方法是将它当作一个条件来使用。

int ival;
cin >> ival;
// 如果在标准输入上键入Boo,读操作就会失败。若输入文件结束标识,cin也会进入错误状态。
 
while (cin >> word)
    // ok: 读取成功
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

while循环检查 >>表达式返回的流的状态。如果输入操作成功,流保持有效状态,则条件为真。

管理输出缓冲

  1. 刷新输出缓冲区可以使用endl、flush和ends
  2. 如果想在每次输出后都刷新缓冲区,可以使用unitbuf操纵符。
  3. 当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出符。
    cout << unitbuf; // 所有输出都刷新缓冲区
    cout << nounitbuf; // 回到正常的缓冲方式

输出刷新缓冲区

cout << "hi!" << endl;  //输出hi和一个换行符,然后刷新缓冲区
cout << "hi!" << flush;  //输出hi然后属性缓冲区
cout << "hi!" << ends;  //输出hi和一个空字符,然后刷新缓冲区
  • 1
  • 2
  • 3

unitbuf操纵符

cout << unitbuf;        // 所有输出都刷新缓冲区
cout << nounitbuf;      // 回到正常的缓冲方式
 
cin >> ival;            // 标准库将cin和cout关联在一起,此语句也会导致cout的缓冲区被刷新
  • 1
  • 2
  • 3
  • 4

我们既可以将一个istream对象关联到另一个ostream,也可以将一个ostream关联到另一个ostream。

cin.tie(&cout);        // old_tie 指向当前关联到cin的流(如果有的话)。这句仅仅用来展示:标准库已经默认将cin和cout关联在一起
ostream *old_tie = cin.tie(nullptr);    // cin不再与其他流关联
cin.tie(&cerr);                         // 读取cin会刷新cerr,而不是cout。这不是一个好主意,因为cin应该关联到cout
cin.tie(old_tie);                       // 重建cin和cout间的正常关联
  • 1
  • 2
  • 3
  • 4

8.2 文件输入输出

除了继承自iostream类型的行为之外,fstream中定义的类型还增加了一些新的成员来管理与流相关的文件。
在这里插入图片描述
自动构造和析构:当一个fstream对象被销毁时,close会被自动调用。

文件模式:每个流都有一个关联的文件模式。
在这里插入图片描述

8.3 string流

在这里插入图片描述

9. 顺序容器

顺序容器:

1)除非有很好的理由选择其他容器,否则使用vector是最好的选择。
2)要求随机访问元素,应该使用vector或deque。
3)要求中间插入或删除元素,应该使用list或forward_list。

增加元素:
c.insert(p, t)

1)在迭代器p指向的元素之前创建一个值为t或由args创建的元素,返回指向新添加的元素的迭代器。

2)注意:向一个vector、string或deque插入元素会使所有指向容器的迭代器、引用和指针失效。

删除元素:
c.erase(p );

删除迭代器p所指的元素,返回一个指向被删元素之后元素的迭代器,若p指向尾元素,则返回尾后迭代器。若p是尾后迭代器,则函数行为未定义。

迭代器:左闭右开区间。

begin和end

begin返回第一个元素的迭代器,end返回尾元素之后位置的迭代器。 r返回反向迭代器,c返回const迭代器。
当不需要写访问时,应使用cbegin和cend.

赋值和swap:

assign仅适用于顺序容器,会替换原来的值。 元素拷贝赋值。 swap交换2个相同类型容器的内容。不会进行元素拷贝。

front,back,下标和at返回的是引用。

c.front() = 42;
auto &v = c.back();  可以改变
auto v2 = c.back();  无法改变
  • 1
  • 2
  • 3

访问元素:

at可以对越界进行检查,越界会抛出out_of_range异常。下标访问不会。

管理容量的成员函数:

c.shrink_to_fit();
只适用于vector。string和deque。将capacity减少为size大小。只是一个请求,标准库并不保存归还内存。

额外的string操作:

substr, 返回一个string,是原始string的一部分或全部的拷贝。 传递一个开始位置和计数值。 string s1 =
s.substr(0, 5);如果开始位置大于s大小,会抛出out_of_range异常。如果开始位置+计数值大于s大小,则只会拷贝到结尾。

find返回一个npos,定义为-1;
auto f = s.find(“name”);

数值转换:

  1. 转换为string. to_string(i);
  2. string转换为其他: stod(dValue); stoi,stol,stoul等。 如果string不能转换为一个数值,会抛出invalid_argument异常。如果转换得到的数值无法用任何类型来表示,会抛出out_of_range异常。

10. 泛型算法

标准库容器定义的操作集合惊人的小。标准库并未给每个容器添加大量的功能,而是提供了一组算法,这些算法中的大多数都独立于任何特定的容器。

概述:

  1. 算法不能直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。例如对于算法find的使用。
  2. 理解算法的最基本方法就是了解他们是否读取元素、改变元素或是重排元素顺序。

只读算法
accumulate:求和算法。

int sum = accumulate(vec.cbegin(), vec.cend(), 0);
// 对vec中的元素求值,和的初值是0。第三个参数的类型决定了函数中使用哪个加法运算以及返回值的类型。
string sum = accumulate(v.cbegin(), v.cend(), string(""));    // 将vector中所有string元素连接
string sum = accumulate(v.cbegin(), v.cend(), "");            // 错误,传递的是字符串字面值,用于保存和的对象的类型将是const char*,没有定义+运算符。
  • 1
  • 2
  • 3
  • 4

equal:用于确定两个序列是否保存相同的值,相同返回true,否则返回false。

equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());
// 将第一个序列中的每个元素与第二个序列中的对应元素比较,元素类型不是必须一样,如可以是vector<string>和list<const char*>
// 接受3个迭代器,前两个接受第一个序列中的元素范围,第三个表示第二个序列的首元素。
  • 1
  • 2
  • 3

写容器元素的算法

fill(vec.begin(), vec.end(), 0); // 将每个元素重置为0
  • 1

用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长。 需要调用者(程序员)确认。

重排容器元素的算法

sort(words.begin(), words.end());                        
// 按字典序排序
auto end_unique = unique(words.begin(), words.end());   
// unique重排输入范围,使得每个单词只出现一次
// 将出现一次的单词排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
words.erase(end_unique, words.end()); 
// 删除重复元素,end_unique到容器尾部保存重复的单词。注意:即使没有重复元素也可以。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

定制操作
Lambda介绍:
为什么要使用Lambda表达式?

1.可以在某个函数的函数体内直接定义,省了一步在外面定义函数的操作;
2.作为函数的参数,类似回调函数。

参数列表:

  1. 参数列表类似普通函数的参数列表,如果没有,()可以省略不写。
  2. lambda函数不能有默认参数。 参数列表和返回类型可以省略。

捕获:

  1. 普通捕获:[变量名] ,值拷贝
  2. 隐式捕获 :[=] [&]
  3. 混合方式捕获:[=,变量名] [&,变量名] 在变量列表使用另一种捕获方式。

Lambda函数的本质

当我们编写了一个lambda函数之后,编译器将它翻译成一个类,该类中有一个重载了()的函数。

注意事项:

1)捕获只能应用与 lambda 被创建时所在作用域内的 “non-static” 局部变量(包括形参)。
2)静态变量无法被lambda捕获,但可以直接使用。
3)无论是按值捕获,还是按引用捕获,都存在依赖生命周期的问题。其中按引用捕获可能会导致悬空引用,而按值捕获则对悬空指针很敏感(尤其是this 指针), 并且容易误导开发者产生 lambda 是独立的想法。

Bind函数

  1. 将参数绑定到函数上,使一些非一元函数能够当作一元函数使用。 调用bind的一般形式如下: auto newCallable = bind(callable, arg_list);
    newCallable是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。简而言之,我们调用newCallable时,newCallable会调用callable,并将arg_list中的参数传递给它。
    arg_list中的参数可能包含形如_n的名字,n是一个整数。这些参数是占位符,表示newCallable的参数。

绑定引用参数:

默认情况下,bind的那些不是占位符的参数被拷贝到bind返回的可调用对象中。但是,有些参数希望以引用方式传递,或者该类型的参数无法拷贝(如os)。
ref返回一个对象,包含给定的引用,这个对象是可以拷贝的。标准库中还有cref函数,生成一个保存const引用的类。

使用:
TClass::mainThread(std::bind(&report::getOne, this));

11.关联容器

关联容器的元素是按照关键字来保存和访问的。

2个主要的关联容器:map和set.

1)map中的元素是一些关键字-值(key-value)对,关键字起到索引的作用,值则表示与索引相关联的数据。
2)set中每个元素只包含一个关键字,支持高效的关键字查询操作:检查一个给定关键字是否在set中。

使用场景:

1)通过map来统计单词在输入中出现的次数。
2)使用set来忽略常见的单词

标准库提供了8个关联容器,1. 关键字是否可以重复 2. 是否有序。
multimap表示可以重复,unordered_map表示无序。
在这里插入图片描述

map是关键字-值对的集合,set是关键字的集合。

pair类型:

1)一个pair保存2个数据成员。 pair<T1, T2> p;进行值初始化 pair<T1, T2> p(v1, v2);
2)first是v1, second是v2 make_pair(v1, v2);
3)返回一个用v1和v2初始化的pair,类型通过v1和v2的类型推断得到。

迭代器操作:

1)map的first不可更改。
2)set的迭代器都是const,不可更改值。

添加元素:

  1. 向map和set中插入一个已经存在的元素,对容器没有任何影响。
  2. 向map添加一个元素:insert的返回值:对于一个不包含重复关键字的容器,返回一个pair。第一个元素表示给定关键字元素的迭代器,第二个元素表示是否插入成功的bool值。如果已经在容器中,bool是false。如果是重复的容器,则返回给定关键字元素的迭代器。

删除元素:

1 . erase函数,返回删除元素的个数。

map下标操作:

map和unordered_map容器提供了下标运算符和一个对应的at函数。set不支持下标,因为没有与关键字对应的值。multimap也不支持下标操作,因为有多个值与关键字对应。
map进行下标操作时,如果关键字不再map里,则会添加到map中,并进行值初始化。所以只能对非const的map使用下标操作。 c[k];
返回关键字为k的元素;如果k不在c中,添加一个关键字为k的元素,并进行值初始化。
c.at(k);访问关键字为k的元素,带参数检查;若k不在c中,抛出一个out_of_range异常。

访问元素:

find,count,count会返回次数,如果不需要计数,最好用find. 对map使用find代替下标操作。

无序容器:

1)不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的==运算符。
2)无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。无序函数的性能依赖于哈希函数的质量和桶的数量和大小。

12. 动态内存

动态内存使用中常见的问题:

  1. 内存泄漏(忘记释放内存)
  2. 产生引用非法内存的指针(释放尚有指针引用的内存)。

智能指针

  1. shared_ptr:允许多个指针指向同一个对
  2. unique_ptr:独占所指向的对象

shared_ptr类

// 智能指针也是模板,必须提供额外的信息:指针可以指向的类型
shared_ptr<string> pi;
shared_ptr<list<int>> p2;
 
if (p1 && p2->empty())    // 在条件语句中使用智能指针,效果是检测它是否为空
    *p1 = "hi";           // 如果p1指向一个空string,解引用p1,将一个新值赋予string
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

make_shared函数:在动态内存中分配一个对象并初始化,返回指向此对象的shared_ptr。

shared_ptr<int> p3 = make_shared<int>(42);            // p3指向一个值为42的int的shared_ptr
shared_ptr<string> p4 = make_shared<string>(10, '9'); // p4指向一个值为"9999999999"的string
shared_ptr<int> p5 = make_shared<int>();              // p5指向一个值值初始化的int,值为0
auto p6 = make_shared<vector<string>>();              // p6指向一个动态分配的空vector<string>
  • 1
  • 2
  • 3
  • 4

shared_ptr的拷贝和赋值

auto p = make_shared<int>(42);    // p指向的对象只有p一个引用者
auto q(p);                        // p和q指向相同的对象,此对象有两个引用者
  • 1
  • 2

一旦一个 shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。

auto r = make_shared<int>(42); // r指向的int只有一个引用者
r = q;  // 递增q指向的对象的引用计数,递减r原来指向的对象的引用计数,r原来指向的对象已经没有引用者,会自动释放
  • 1
  • 2

每个shared_ptr都有一个引用计数

  1. 当我们用一个shared_ptr初始化另一个shared_ptr时,或将它作为一个参数传递给一个函数,以及作为函数的返回值时,它所关联的引用计数就会递增。当我们给一个shared_ptr赋予新值或者shared_ptr被销毁时,引用计数会递减。
  2. 每次shared_ptr销毁时,会调用shared_ptr的析构函数,将引用计数-1,如果此时变为0,则会销毁对象并释放所占用的内存。

直接管理内存
使用new动态分配内存和初始化对象。

int *p1 = new int; 默认初始化,值未定义 int *p2 = new int(); 值初始化为0; int *p3 =
new (nothrow) int; 如果分配失败,会返回一个空指针,不会抛出bad_alloc异常。

释放动态内存

  1. delete会销毁给定指针指向的对象;释放对应的内存。
  2. 传递的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存或者将相同的指针值释放多次,行为是未定义的。
  3. delete之后,指针变为悬空指针,指向一块曾经保存数据对象但现在已经变得无效的内存的指针,应该delete后将其赋nullptr。

shared_ptr和new结合使用

shared_ptr<double> p1;            // shared_ptr可以指向一个double
shared_ptr<int> p2(new int(42));  // p2指向一个值为42的int
shared_ptr<int> p3 = new int(42); // 错误!必须使用直接初始化方式,智能指针构造函数是explicit的。 
  • 1
  • 2
  • 3

不要混合使用普通指针和智能指针

  1. 如果混合使用的话,智能指针自动释放之后,普通指针有时就会变成悬空指针,当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。
  2. 也不要使用get初始化另一个智能指针或为智能指针赋值

unique_ptr
某个时刻只能有一个unique_ptr指向一个给定对象,由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。
在这里插入图片描述
不能拷贝unique_ptr有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr.最常见的例子是从函数返回一个unique_ptr.
在这里插入图片描述
weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。

weak_ptr的操作
在这里插入图片描述

由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否存在。如果存在,lock返回一个指向共享对象的shared_ptr,如果不存在,lock将返回一个空指针。

使用场景:

  1. 解决循环引用无法释放的问题
  2. 探查内存空间是否有效

高级:类设计者的工具

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号