CPP初级
语雀文章链接:https://www.yuque.com/tintoki/znb576/hizahi(之前在语雀跟着B站黑马的视频也做过一些整理,感兴趣可以参考)
本篇博客是对《C++ Primer Plus》书中相关知识点的总结(顺带说一点,这本书对于刚学C++的新手非常友好,对我们这种基础不牢后天补习的也非常友好,知识点覆盖非常全面)
2022/7/12 11:26 读这本书的时候千万别忙着刷一遍什么的,一定一定要动脑子想以及回顾之前的内容,我们不是初学!!!这本书是对C++的一个详细的剖析的书,不要追求速度而要弥补以前学习C++过程中忽略掉的知识点;
一、绪论
C++在C语言的基础上添加了面向对象编程(面向对象的特性)和泛型编程(C++的模板特性);
过程性编程(如C语言)强调算法、自顶向下,OOP强调数据、自底向上;
类规定了可以使用哪些数据来表示对象(属性)以及可以对这些数据执行哪些操作(方法)——属性和方法统称为类的成员;
泛型编程提供了执行常见任务的工具(STL标准模板库就是一种泛型编程),范型指的是创建独立于类型的代码(也就是无论数据类型是哪种,可以通用一种形式的范型函数处理);
C++对大小写敏感;
注意,模块和模板根本不是一个维度上的概念;
模块是一种编程思想,简称模块化编程,也就是自底向上的编程思想;
模板是C++的新特性,是一种通用程序设计技术,与程序和数据类型无关,可以实现模板函数、模板类,而大名鼎鼎的STL也是利用C++的模板技术实现的,供用户直接使用模板类或模板函数;
关于Windows的编译器
需要注意的是,程序能够通过某个编译器的编译并不代表它就是合法的C++程序(比如它可能会在后续链接库的时候出问题,或者它仅仅是个合法但是没有语义的语句等);
二、C++基础
运行C++程序是从程序的main()函数开始的,这启发我们在分析一个C/C++程序的时候也应当从main函数入手逐步解析每个函数、对象的用法(不同于python自上而下);
1.include指令
1 |
|
这是一条预编译指令,将导致在预处理阶段预处理器将iostream文件的内容添加到程序中——这是一种典型的预处理操作,在源代码编译之前替换或添加文本;
iostream这样被包含在其他文件中的文件称为包含文件或头文件,C语言传统头文件使用拓展名.h,纯粹的C++头文件无后缀名,更多命名约定如下
关于#include为什么只需要引入头文件就可以使用源文件中的函数,以及头文件中需要写一些什么,这些都属于远古难题(至少我之前在学习的过程中一直没有注意,然后用习惯之后再回过头来看发现好像自己对这个知识点也是一点都不了解)—这部分涉及很深奥的C底层链接相关,然而在《程序员的自我修养》这本书里尽管对链接讲的很多,但是对include的原理涉及很少,而之前在某些地方看到的“include作为文件的接口”这种说法有一定道理,但是在严格意义上的链接是在编译之后而非编译之前进行—暂时不要纠结这个问题,先把基础的知识点学了,之后再研究这个问题;
关于上面的问题可以看第六章的第一节编译,那里我做了简单回答;
2.名称空间
名称空间的支持是C++的一项特性(C没有名称空间这一说),名称空间可以使厂商将产品封装在名称空间单元中,使用者在使用时可以指定使用某个名称空间单元的产品
名称空间的声明有以下几种方式
1 |
|
3.关于cout
cout实际上是一个输出流对象,它拥有一个属性称为插入运算符(<<),这个插入运算符实际上利用了C/C++的运算符重载特性,编译器通过上下文确定重载运算符代表的含义是什么;
插入运算符(<<)将右侧信息插入到cout输出流对象中,(<<)符合指明了信息流的流动路径;
4.声明语句
要将信息存储在计算机中,必须指出信息的存储位置和所需要的存储空间;在C++中使用声明语句来指出存储类型(分配对应需要的内存空间)并提供位置标签(内存单元的名称或地址);
除了上述介绍的简单的在首次使用变量之前声明该变量的定义声明
,还有一种引用声明
——命令计算机使用在其他地方定义的变量(extern语句,这个之后应该会介绍);
python中的变量赋值不需要类型声明,但是每个变量在使用之前一定要赋值(实际上这也算是一种声明),只有给变量赋值以后该变量才会被创建;
5.函数原型
在使用函数之前,C++编译器必须知道函数的参数类型和返回值类型——C++提供这种信息的方式是使用函数原型
语句;
函数原型与函数的关系类似于变量声明与变量的关系,C++程序应当为程序中使用的每一个函数都提供原型;
函数原型实际上就是在函数头后面添加一个分号,表明它是一条语句;
函数原型可以在main()前面或者main()里面,但是一定要在函数调用之前;
实际上在我们刚学习C++的时候使用的大部分函数都没有用过函数原型,但是在大型项目开发的过程中必须要使用函数原型;
这是因为对于简单的程序可以通过改变函数定义的位置来避免出现’undeclared identifier’的错误,但是对于复杂的,需要相互调用的函数之间就必须使用函数原型提前声明;
而当我们使用一些C++内置函数的时候,只需要包含相应的头文件即可无需额外声明(头文件中已经包含了原型的声明,注意头文件中严格来说不要定义函数只能声明原型,函数的定义在库文件中)
main()函数的返回值并不是返回给程序的其他部分,而是返回给操作系统——许多操作系统都可以实用程序的返回值(也就是main()的返回值)
Q:为什么需要函数原型?
A:原型描述了函数到编译器的接口,它将函数返回值的类型以及参数的类型和数量告诉编译器;假如没有这一系列措施会导致编译器的效率被严重拉下来(去文件中挨着找函数定义)甚至根本找不到函数定义
- 原型确保编译器正确处理函数的返回值;
- 原型确保编译器检查使用的参数数目是否正确;
- 原型确保编译器检查使用的参数类型是否正确;
三、数据类型
面向对象编程的本质就是设计并拓展自己定义的数据类型,在创建自己的类型之前,非常有必要了解并理解C++内置的数据类型
C++内置数据类型主要分为两类:
- 基本类型:整型(存储为整数的值构成)和浮点型(存储为浮点格式的值构成)
- 复合类型:数组、字符串、指针和结构
在学习数据类型之前我们首先要明白数据类型有什么用,为了将信息存储在计算机中,程序必须记录信息的基本属性:
信息存储的内存地址
C++使用&运算符检索变量的内存地址存储何种类型的信息
C++不同的数据类型
通过使用不同数目的位来存储和表示值存储的信息的值
———-基本数据类型———-
1.整型
1.1 整型概述
C++的基本整型有char short int long 以及long long,每种类型都有有符号版本和无符号版本(无符号版本可以增大变量能够存储的最大值但是不能增大变量的表示范围);
char有一些特殊属性,故常用它表示字符而非数字
不同的操作系统中,C++的数值类型长度不是固定的(当然我们希望它是固定的),C++制定了一种灵活的标准确保最小长度的规范性:
- short至少16位;
- int 至少与short一样长;
- long至少32位,且至少与int一样长;
- long long至少64位,且至少与long一样长;
除非有理由存储为其他类型,否则C++默认将整型常量如123存储为int类型;
之前我们没有注意到这个知识点,这里详细说一下;
我们知道C++在创建变量的时候会对其进行声明显式指明其数据类型,但是创建常量的时候是直接使用诸如#define MAX 123,这种语句并没有显式的指明MAX常量的数据类型,这时候上面的规则就起到了作用——只要没有使用特殊后缀表示特定数据类型或者值超出了int类型的表示范围,C++统一默认整型常量的数据类型为int(至于其他常量就只能以后碰到再查资料了)
1.2 头文件limits
头文件climits(转换后的C头文件)定义了符号常量来表示对数据类型的限制,如前所述,INT_MAX表示类型int能够存储的最大值;
下图对该文件中定义的符号常量进行了总结;
1.3 define指令
#define和#include指令一样,都是预处理器编译指令;
define告诉预处理器,在程序中找到 INT_MAX并将所有的INT_MAX替换为32767,修改后的程序将在完成这些替换后被编译;
#define是C遗留下来的一种创建符号常量的方式,C++提供了一种更好的方式——使用关键字const,将在后面介绍;
1.4 变量初始化
初始化是将变量的声明和赋值合并在一起,也就是在声明变量的同时为变量赋值;
1 |
|
1.5 char类型
char类型是专为存储字符(字母或数字,注意不能是字符串)而设计的;
因为计算机通过使用字母的数值编码(如ASCLL码)存储字母(C++对字符用单引号,对字符串用双引号),故char类型也是一种整型,它足够长以能够表示计算机系统中的所有基本符号——字母、数字、标点符号等;
关于字符类型字面值的表示:单引号是字符型,单引号引起的一个字符实际上代表一个整数。 双引号是字符串型,双引号引起的字符串,代表的却是一个指向无名数组起始字符的指针;
However,在python中用’’和””都可以表示字符串,因为python根本就没有字符类型,只有字符串类型;
用一个字节就可以表示所有的符号,故常令char宽度为1字节;
char在
默认情况下
既不是无符号也不是有符号,是否有符号由C++实现来决定,当然也可以显式指定unsigned char bar或者signed char bar;
1.6 const限定符
相较于#define预编译指令,const限定符定义常量有多种好处,具体可以查阅网上资料;
符号常量
(constant) 即添加了const的定义的量,字面常量
(literal constant) 即无需定义可以直接写出来的量如123;
下面是使用const限定符定义常量的方式
通过这样定义之后可以在程序中使用Months而不是12(这样的好处是我们可以轻松的读出语句的含义,而如果直接使用12可能会造成误解,是12个月还是12个小矮人???)
使用const限定符一定要在声明常量的同时给常量赋值;
2.浮点型
C++有三种浮点类型:float double 和 long double,这些类型按照它们可以表示的有效位数和允许的指数最小范围来描述的;
- float的有效位至少是32位;
- double的有效位至少是48位,且不少于float;
- 这三种类型的指数范围至少是-37到37;
默认情况下,浮点常量都属于double类型,如果希望指定常量为float类型,可以使用f或F后缀,指定常量为long double类型可以使用l或L后缀;
求模运算符%生成第一个数除以第二个数后的余数,其两个操作数都必须是整型,千万不能将该运算符作用于浮点数;
3.类型转换
C++在这些情况下自动执行多种类型转换:
将一种算术类型的值赋给另一种算术类型的变量时,C++将对值进行转换;
表达式中包含不同的类型时,C++将对值进行转换;
将参数传递给函数时,C++将对值进行转换;
3.1 初始化和赋值进行的转换
C++允许将一种类型的值赋给另一种类型的变量,这将导致值被转换为接收变量的类型;
将一个值赋给取值范围更大的类型通常不会出现问题,但是将一个很大的long值赋给float变量将降低精度
下面给出了一些数值转换过程中可能出现的问题
3.2 列表初始化进行的转换
前面我们介绍了使用大括号初始化器进行初始化的操作,这也称为列表初始化;
列表初始化不允许缩窄、不允许将浮点型转换为整型;
3.3 表达式中的转换
Q:当同一个表达式中包含两种不同的算数类型会出现什么后果?
A:C++将执行两种自动转换:
- 首先,一些类型在出现时使会自动转换——计算表达式的时候C++将bol char short等转换为int类型的值;
- 其次,有些类型在与其他类型同时出现在表达式中时将被转换——较小的类型被转换为较大的类型;
3.4 强制类型转换
强制类型转换不会修改变量本身的值,而是创建一个新的、指定类型的值
3.5 auto声明
C++l1新增了一个工具,让编译器能够根据初始值的类型推断变量的类型(这个和直接在内存中选择某种数据类型保存字面量不一样,这个是赋值给变量进而推导变量的类型);
auto是一个C语言关键字,但很少使用,有关其以前的含义,请参阅第9章。在初始化声明中,如果使用关键字auto,而不指定变量的类型,编译器将把变量的类型设置成与初始值相同:
———-复合数据类型———-
4.数组
下面介绍的数组、结构、共用体都属于数据格式
,数据格式是指数据保存在文件或记录中的编排格式,由数据类型及数据长度来描述(数据格式并不完全等同于数据类型,所以没有通用的数组类型或结构类型,只有具体的自定义的类型);
复合类型基于基本整型和浮点类型创建:
- 影响最深远的复合类型是类,string类提供了一种处理字符串的途径;
- 数组可以存储多个同类型的值,一种特殊的数组可以存储一系列字符;
- 结构可以存储多个不同类型的值;
- 指针是一种将数据所处位置告诉计算机的变量;
下面我们将逐个介绍这些符合类型(类将在之后介绍);
数组是一种数据格式,能够存储多个同类型的值,每个值存储在一个独立的数组元素中,计算机在内存中依次存储数组的各个元素;
数组之所以被称为复合类型(C语言中叫做派生类型),是因为它是基于其他类型创建的;
因为没有通用的数组类型,所以我们不能说float loans[5]是”数组”类型的,而应该说是”float数组”类型的;
sizeof运算符返回类或者数据对象的长度(单位为字节),将sizeof运算符作用于数组名返回整个数组的字节数,将sizeof作用于数组元素返回该元素的长度;
创建数组格式如下:
1 |
|
表达式arraySize指定元素数目,它必须是整型常数(如10)或const值,也可以是常量表达式(如8*sizeof(int)),即其中所有的值在编译时都是已知的;
具体地说,arraySize不能是变量,因为变量的值是在程序运行时设置的;
数组的初始化规则:
- 只有在定义数组时能够对数组进行初始化(使用初始化列表赋值),在之后就只能通过数组下标分别给数组中的元素赋值;
- 使用列表初始化方式对数组进行初始化时禁止缩窄转换(如同时将浮点型数值和整型数值放在同一个列表中,这将导致浮点型缩窄为整型再赋值给数组元素)
5.字符串
字符串不是一种数据类型
,复合类型也算不上,这就好比字符不是一种数据类型
、数字也不是一种数据类型
,只是可以使用某些数据类型如char int来存储和表示它们罢了;
字符串是存储在连续字节内存中的一系列连续的字符,C++处理字符串的方式有两种:
- C风格字符串
- 基于string类
5.1 C风格字符串
字符串存储在连续内存中意味着字符串可以存储在char数组中,我们将这种字符串的数据类型称为char数组类型,这就是C风格字符串;
C风格字符串的特殊性质:以空字符null character结尾;
这两个数组声明都是char数组,但是只有第二个数组存储的是字符串(空字符对C风格字符串尤其重要,字符处理函数如cout逐个处理字符串中的字符直到空字符);
除了上述使用大括号初始化器初始化字符数组以外,还可以直接使用下面的方式初始化字符数组
这种方式不需要显式地包括字符串结尾的空字符;
sizeof运算符指出整个数组的长度,strlen()函数返回存储在数组中的字符串的长度而不是数组本身的长度;
疑问:运算符和函数的区别?
- 运算符只能重载,不能自定义,函数的名字随便起,只要是个标志符就行(当然内置函数除外);
- 运算符/操作符不需要包括任何标题来使用它,但是一个内置函数必须要包含相关头文件;
5.2 关于cin
假如我们编写一个程序,用于获取用户从键盘的输入
1 |
|
这个程序乍一看是没问题的,而且平时我们在使用过程中也的确如此(因为平时我们输入中文从来不会输入空格),假如我们输入的英文姓名以空格分隔
上面结果意味着name只保存了输入的Jim,究其原因是因为无法通过键盘输入结束字符’\0’所以cin使用空白(空格、制表符tab和换行符)来确定字符串结束的位置,因此这就导致cin认为Jim tobi是两个字符串,它只会将先获取的Jim读入到数组name中;
上述问题可以使用getline()和get()函数来解决:
getline()和get()函数都读取一行输入,直到换行符;
- getline()会将换行符接收并替换为结束字符;
- get()会将换行符保留在输入序列中;
要调用getline()方法可以使用cin.getline(),该方法有两个参数:
- 第一个参数用于存储输入行的字符数组的名称;
- 第二个参数是能够读取的最大字符数,如果这个参数为20则最多读取19个字符,最后的空间用于存储自动添加的结束字符;
- getline()通过换行符确定一行字符串的结束,接着将换行符转换为结束字符并保存;
get()成员函数有多种变体(函数重载,也就是函数参数不同),其中一种变体和getline()的参数、作用完全相同,只是get()读取到换行符后会将这个换行符保留在输入流中,这将导致紧接着第二次的get()在输入流中看到的第一个字符就是换行符,因此不会等待用户输入并enter而是直接结束第二次的get()调用;
解决方法是使用get的另一种变体,这种get()不携带任何参数可以读取任何下一个字符(包括换行符),也就是我们在第二次调用cin.get(name1,20)之前先用cin.get()把输入流中的换行符接收,这样就看不到遗留的输入符了;
关于使用字符数组处理字符串实际上还有很多其他问题,我们就不再赘述,一般地,C++使用指针而非数组来处理字符串(这将在之后介绍);
5.3 string类
可以使用string类的对象而非字符数组来存储字符串,这提供了一种将字符串作为一种新的数据类型的表示方法(这句话就是导致我们之前一直认为字符串是数据类型的原因);
要使用string类需要包含位于std名称空间中的头文件string,string类隐藏了字符串的数组性质,使我们可以像处理普通变量一样处理字符串;
使用string对象的方式与使用字符数组的方式处理字符串的相同点:
可以使用C-风格字符串来初始化string对象(大括号初始化器或字符串);
可以使用cin来将键盘输入存储到string对象中;
可以使用cout来显示string对象;
可以使用数组表示法来访问存储在string对象中的字符;
使用string对象的方式与使用字符数组的方式处理字符串的不同点:
- 可以将string对象声明为简单对象而不是数组;
1 |
|
类设计可以让程序能够自动处理string对象的大小,相比于使用数组,使用string对象更加安全;
不能将一个数组赋值给另一个数组,但是可以将一个string对象赋值给另一个string对象;
6.结构
数组可以存储多个元素,但所有的元素类型必须相同;
结构是一种比数组更灵活的数据格式,同一个结构中可以存储多种类型的数据;
在具体的结构类型中,使用成员运算符(.)来访问各个成员(访问类成员函数的方式就是从访问结构成员的方式衍生而来)
与数组类似,结构并不是一种具体的数据类型,没有通用的结构类型,因此不能说某个变量是结构类型的,只能说是自定义的如inflatable结构类型;
- C++不提倡使用外部变量/全局变量声明,但是推荐使用外部结构声明;
- 与数组不同,可以使用赋值运算符(=)将结构赋值给另一个同类型的结构(即使结构成员是数组,结构中的每个成员将会被设置为另一个结构中相应成员的值);
另一种声明结构的方式是声明没有名称的结构类型(匿名结构类型),同时定义一种这种结构类型的变量
1 |
|
7.共用体
- 共用体是一种数据格式(数组、结构都属于数据格式),它在能够存储不同的数据类型,但在同一时间只能存储其中的一种类型(也就是说结构可以定义并存储float double;共用体可以定义float double的但只能在一个时间要么存储float要么存储double)
1 |
|
共用体的作用之一是当数据项使用两种或更多数据类型(但不会同时使用),可以节约空间,如有一些商品的price为整数有一些商品的price为浮点数;
8.枚举
C++的enum工具提供了相较于#define const的另一种创建符号常量的方式,使用enum的句法与使用结构相似;
1 |
|
枚举同样不是一种具体的数据类型,借助枚举定义来创建新的自定义枚举类型;
1 |
|
8.1 枚举常量
- 可以使用赋值运算符来显式地设置枚举量的值:
1 |
|
- 也可以只显式地定义其中一些枚举量的值:
1 |
|
- 可以创建多个值相同的枚举量;
1 |
|
- 在C++早期的版本中,只能将int值(或提升为int的值)赋给枚举量,但这种限制取消了,因此可以使用long甚至long long类型的值。
9.指针
首先明确一点,指针变量属于复合类型变量的一种;
我们之前都是使用变量声明这种策略来解决计算机程序在存储数据时必须要跟踪的3种基本属性
下面我们介绍以指针变量(通常不讨论指针类型,我们后面简称的指针都是指指针变量)为基础的这种策略;
- 任何一个变量都可以分为变量名和变量值(包括指针变量):
- 变量名是程序可操作的存储区域的名称;
- 变量值是变量名指向的地址中存储的内容—普通变量的变量值就是一个值,指针变量的变量值是一个地址,引用变量的变量值是一个变量名;
- 对普通变量名使用地址运算符(&)可以得到其指向的地址;
- 对指针名使用解除引用运算符(*)可以得到指针指向的地址存储的值;
9.1 声明指针
- 指针声明必须指定指针指向的数据的类型—因为不同数据类型使用的字节数不同,并且存储时使用的内部格式也不同;
1 |
|
- 指针变量p_updates的类型是指向int的指针也就是int*类型;
- *p_updates变量的类型为int;
1 |
|
指针同样不是通用的,对一个指针变量不能简单地说它是指针类型,而应该说它是指向int的指针类型或者指向char的指针类型…
9.2 new运算符
首先我们要知道,直接创建指针并给*指针赋值会出问题
1 |
|
因此在对指针使用(*)之前一定要将指针初始化为一个确定的、适当的地址;
- 最简单的方法就是直接fellow=&A(A是某个已经存在的普通变量),在编译时将指针初始化为变量的地址—这种方式下指针只是作为别名可以直接访问内存;
1 |
|
- 第二种方法就是在运行阶段分配分配未命名的内存,此时只能通过指针来访问该内存并用以存储值;
1 |
|
这两种方式都将int变量的地址赋值给了指针pn,但是第一种情况可以通过A或指针pn来访问,第二种情况只能通过pn来访问—因为第二种情况下pn指向的内存没有名称
,我们称之为数据对象(为数据项分配的内存块),为一个数据对象获得并指定分配内存的通用格式如下:
1 |
|
使用new分配的内存块(第二种方式)通常与常规变量声明分配的内存块(第一种方式)不同:
- 变量存储在栈区中;
- new从堆区/自由存储区中分配内存;
当内存被耗尽,无法满足new的需求时,将返回空指针(值为0的指针),C++会确保空指针不指向有效数据;
9.3 delete运算符
一定要配对使用new和delete,否则会出现内存泄漏,也就是说被分配内存再也无法使用;
不要尝试释放已经释放过的内存块,这将导致任何可能的结果;
不要使用delete来释放声明变量获得的内存,只能用delete来释放使用new分配的内存—不要创建两个指向同一内存的指针,这将增大错误地删除同一个内存块两次的可能性;
delete只会释放指向的内存但不会删除指针本身,可以令指针指向新的内存;
1 |
|
9.4 指针和数组
前面介绍动态数组时说到C/C++的指针和数组几乎等价,其原因在于指针算数和C++内部处理数组的方式;
9.4.1 指针算数
多数情况下,C++将数组名解释为数组第一个元素的地址(对所有数组,不只是动态数组);
将整型变量+1后其值将增加1;
将指针变量+1后,增加的量等同于它指向的类型的字节数—将double指针+1后指针值将增加8,将short指针+1后指针值增加2;
指针和数组名的区别
- 在多数表达式中,指针和数组名都表示地址,区别就是可以修改指针的值但是数组名是常量不能修改;
- 对数组应用sizeof运算符得到的是数组的长度(数组长度*数组元素类型所占字节数=数组总字节数),对指针应用sizeof得到的是指针的长度(即使指针指向的是一个数组),这种情况下C++不会将组名解释为地址;(长度和所占字节数很多时候都会被混淆,但是我们只需记住它们是相关的即可,其他细节具体再查阅资料即可)
9.4.2 数组地址
既然前面说大多数时候将数组名解释为地址,那对数组取地址(也就是使用&运算符)情况如何?
- 数组名被解释为地址通常都是指的其第一个元素的地址;
- 对数组名使用(&)运算符时得到的是整个数组的地址;
1 |
|
数值上来说这两个地址是相同的,但是概念上来说&tell[0]也就是tell是一个2字节的内存块地址,而&tell是一个20字节内存块的地址
9.4.3 字符数组
数组和指针的这种特殊关系可以拓展到C风格字符串,因为C风格字符串本质就是字符数组;
因此在cout和多数C++表达式中,char数组名、char指针以及”字符串常量”都被解释为字符串的第一个字符的地址;
因此我们也就知道为什么使用C++ string类型处理字符串更加简单—不用担心数组越界,将字符串赋值直接使用赋值运算符(=)而不是strcpy()函数或strncpy()函数;
9.4.4 动态数组
对于管理小型数据对象,只需要声明一个简单的变量即可(不必使用new运算符),这样做比指针更加简单;
new运算符真正的用武之地在于处理大型数据(数组、结构和字符串):
- 在编译时给数组分配内存被称为
静态联编
,这样的数组称为静态数组,无论是否使用该数组都会占用内存; - 在程序运行时根据情况创建数组并为它分配内存称为
动态联编
,这样的数组称为动态数组;
为数组分配内存的通用格式如下
1 |
|
- 示例
1 |
|
使用new创建的数组,需要使用另一种格式的delete来释放;
1 |
|
不能使用sizeof运算符来确定动态分配的数组包含的字节数(因为程序自动跟踪的动态数组分配的内存量这种信息不会公开)
因为psome指向的是数组的第一个元素,所以*psome就是数组第一个元素的值;
然而我们并不需要那么麻烦的做法—直接将指针作为数组名使用即可:
- 对于第一个元素可以使用psome[0]访问;
- 对于第二个元素可以使用psome[1]访问;
这样做的原理是C/C++中数组和指针几乎等价;
9.5 指针和结构
运行时创建数组(动态联编优于编译时创建数组(静态联编)),同样适合结构;
将new用于结构由两步组成:创建结构和访问其成员:
1.创建结构需要同时使用结构类型和new
1 |
|
2.访问动态结构的成员时,不能将成员运算符(.)用于结构名,因为这种结构根本没有结构名(类似于9.2中所说的ps指向的内存没有名称),只知道该内存的地址
箭头运算符(->):
- 该运算符可用于指向结构(实际上是指向该结构变量的地址,不引起混淆的话可以简称)的指针(类同理),就像点运算符(.)可用于结构名;
- 注意箭头运算符的左边一定是指针!
例:ps->price是被指向的匿名inflatable结构变量的price成员
- 如果结构标识符是结构名则使用句点运算符;
- 如果结构标识符是指向结构的指针则使用箭头运算符;
另一种方法是结合使用引用解除运算符(*)和句点运算符(.)
例:(*ps).price是被ps指针指向的匿名inflatable结构变量的price成员
9.6 指针和const
可以使用两种不同的方式将const关键字用于指针:
- 第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值;
- 第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置;
10.类型别名
C++为类型建立别名的方式主要有两种:
1.使用预处理器
1 |
|
预处理器在编译程序之前用所有的char替换所有的BYTE,从而实现使BYTE成为char的别名;
2.typedef关键字
- 格式
1 |
|
- 举例
1 |
|
typedef不会创建新类型,只是为已有的类型建立一个新名称;
typedef和匿名结构、匿名枚举搭配会有意想不到的效果(也就是将匿名变为有名);参考C语言定义结构体的几种方法 - 百度文库 (baidu.com)
11.引用变量
C++11新增了一种复合数据类型 —— 引用变量(本质上来说引用和指针是一样的,但是引用更加美观,指针是引用的底层实现,引用是操作受限的指针);
引用变量的主要用途是作为函数的形参,通过将引用变量用作参数函数将直接使用原始数据而不是拷贝副本
Q:为什么C++有了指针还需要引用?指针作为函数的参数不是一样很方便?
A:参考自知乎回答既然有指针了,为什么c++还搞个引用出来? - 知乎 (zhihu.com)以及引用和指针 - 走看看 (zoukankan.com)
11.1 创建引用变量
C/C++使用&符号指示变量的地址,但是C++为&赋予了新的含义——将其用来声明引用
1 |
|
int&指的是指向int的引用(char*是指向char的指针),上述A和B指向相同的值和内存单元
必须在声明引用的时候将其初始化(指针可以先声明再赋值)
引用更加类似于const指针,必须在创建时初始化,一旦同某个变量关联起来就将一直效忠该变量
1 |
|
11.2 常量引用
假如只想让函数使用参数但不想对这些信息进行修改,同时又想使用引用,应当使用常量引用
1 |
|
11.3 引用时机
使用引用参数的原因:
- 程序员能够修改调用函数中的数据对象;
- 通过传递引用而不是整个数据对象,可以提高程序的运行速度;
上述两个原因似乎指针也能实现?那么值传递、指针传递、引用传递使用时机应当是:
函数使用传递的值而不做修改
如果数据对象很小,如内置数据类型或小型结构,则按值传递;
如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针(C++里没有数组的引用【引用】数组不能有引用 - 海山 - 博客园 (cnblogs.com));
如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间;
如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递((19条消息) C++为什么要用引用而不是指针_ZJE_ANDY的博客-CSDN博客,当然你要使用指针也没错,C++是很宽容的);
四、文件
使用cin进行输入时,程序将输入视为一系列的字节,其中每个字节都被解释为字符编码。不管目标数据类型是什么,输入一开始都是字符数据——文本数据。然后,cin对象负责将文本转换为其他类型;
(个人感觉Primer上面这一章讲的稀里糊涂的,随便看看就行了,文件输入/输出也不是重点,具体使用的时候看雨雀或者中文网就可以了)
1.文件输出
我们可以类比控制台输出和文本输入(注意文本输入等于文件输出
,即写入文本到文件中),首先控制台输出需要具备以下必要条件:
必须包含头文件iostream;
头文件iostream定义了一个用处理输出的ostream类;
头文件iostream声明了一个名为cout的ostream变量(对象);
必须指明名称空间std;例如,为引用元素cout和endl,必须使用编译指令
using
或前缀std::
;可以结合使用cout和运算符<<来显示各种类型的数据;
文件输出/文本输入与此类似:
文件输出必须包含头文件fstream;
头文件fstream定义了一个用于处理输出的ofstream类;
需要声明一个或多个ofstream变量(对象),并以自己喜欢的方式对其进行命名,条件是遵守常用的命名规则;
必须指明名称空间std;例如,为引用元素ofstream,必须使用编译指令
using
或前缀std::
;需要将ofstream对象与文件关联起来,为此,方法之一是使用open()方法,使用完文件后,应使用方法close()将其关闭;
可结合使用ofstream对象和运算符<<来输出各种类型的数据;
文件输出一定要声明自定义的ofstream对象并为其命名,并将其同文件关联起来;
2.文件输入
五、函数
绪论章节简单介绍过一下函数,但是函数作为C++核心编程模块,仅仅用一小节来讲肯定是不完善的,所以我们这里花一些篇幅来仔细研究C++中的函数
1.函数基础
函数返回值限制:对于有返回值的函数,必须使用返回语句return,C++对于返回值的类型做限制:不能是数组,但可以是其他任何类型(指针、结构甚至是对象),尽管C++函数不能直接返回数组,但是可以将数组作为结构或对象的一部分返回;
函数返回值机制:函数通过将返回值复制到指定的CPU寄存器或内存单元来将其返回;
函数返回值规则:函数在执行返回语句后结束,如果函数包含多条返回语句则函数在执行遇到的第一条返回语句后结束(一般编译器会警告不允许函数有多条return语句);
在函数中声明的变量(包括参数)是该函数私有的,这样的变量被称为局部变量,也被称为自动变量(因为它们在程序执行过程中自动被分配和释放)
2.函数和数组
(这个地方是一个津津乐道的值得深究的问题,书上讲的云里雾里的可以先跳过)
C++和C在大多数情况下都将数组名视为指针,C++将数组名解释为其第一个元素的地址
1 |
|
- C++中当且仅当用于函数头或函数原型中,int*arr和int arr[]含义才是相同的,都意味着arr是一个int指针;
- int arr[]表示arr不仅指向int,同时还指向int数组的第一个int元素;
3.函数和结构
与数组名就是数组第一个元素的地址不同的是,结构名仅仅只是结构的名称,要获得该结构的地址必须使用地址运算符&;
使用结构进行编程最常使用的就是将结构作为参数传递或在需要时将结构作为函数返回值使用:
- 按值传递:当结构比较小时按值传递最合理
- 传递结构的地址:节省时间和空间
- 按引用传递
4.函数指针
函数指针是多态、回调函数的基础;
与数据项类似,函数也有地址,函数的地址是存储其机器语言代码的内存的开始地址
- 获取函数的地址:函数名就是函数地址,think()是一个函数则think就是该函数的地址;
- 声明函数指针:声明指向函数的指针时,必须指定指针指向的函数类型(函数的返回类型以及函数的参数列表)
1 |
|
*pf代表是一个函数,pf代表这是函数指针,为了提供正确的运算符优先级需要在声明中使用括号:
- *pf(int)意味着pf()是一个返回指针的函数
- (*pf)(int)意味着pf是一个指向函数的指针
5.内联函数
内联函数是C++为了提高程序运行速度做的改进,常规函数和内联函数之间的主要区别不在于编写方式,而在于C编译器如何将它们组合到程序中;
常规函数的调用会使得程序跳到另一个地址(函数地址),并在函数结束后返回:执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入到寄存器中),然后跳回到地址被保存的指令处,来回跳跃并记录跳跃位置意味着以前使用函数时需要一定的开销;
C++内联函数提供了另一种选择。内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本(因此内联函数的函数体应当尽量小);
- 实现内联函数需要在函数声明/定义前加关键字inline;
- 通常省略函数原型(因为内联函数体较小),将函数定义放在原本应该放置原型的位置;
1 |
|
- 内联函数不能递归;
6.函数重载
函数多态又称为函数重载,能够使用多个同名的函数;
函数重载的关键是函数的参数列表,也称为函数特征标(参数类型、参数数目,但不包括函数返回类型);
编译器在匹配重载函数时,不会区分const和非const变量,但是会根据是否合法来选择是否匹配;
重载时机:函数重载不要滥用,仅当函数
基本上执行相同的任务
(否则就不要使用同名函数)但是使用的是不同形式的数据时才应该使用函数重载;
7.函数模板
函数模板是通用的函数描述,它们使用泛型来定义函数,其中的泛型可用具体的类型(int char double)来替换;
函数模板同样需要使用原型声明
1 |
|
函数模板不能缩短可执行程序的执行时间或者大小(简化程序代码),它仅仅方便编程人员编写程序;
函数模板也能使用重载,函数原型如下
1 |
|
当然,编写的模板函数可能无法处理某些类型的数据(如数组、结构),有两种解决方式,一种是重载运算符使其适合特殊的类型的数据,另一种解决方案是为特定类型提供具体化
的模板定义
隐式实例化(声明对象时指出所需类型)、显式实例化(声明类时指出所需类型)和显式具体化统称为具体化,它们都表示的是使用具体类型的函数定义(而不是如同函数模板一样的通用描述)
六、内存模型
1.编译
- 头文件:包含结构声明、函数原型、类声明、#define或const定义的符号常量、模板声明、内联函数;
- 源文件:函数定义、main()入口函数;
不要将函数定义或变量声明放在头文件中;
不要使用#include来包含源文件,这将导致多重声明;
不要将头文件加入到项目列表中(而IDE常常帮我们自动完成了这件事,其实这句话的本意是让我们不要把头文件加入编译列表中);
不要再头文件中使用using编译指令(using namespace std;)这可能会导致名称的混淆;
关于上面这个问题,至少现在对于我们做的这些简单的项目来说无论是把头文件加入项目列表还是把头文件直接加入编译列表(头文件本质上不需要单独编译,会随着源文件一起被编译)这样编译器都不会报错,C++是一门包容性很强的语言,所给的规则最好遵守(当然不遵守有些编译器也不会报错);
同一个文件中只能将同一个头文件包含一次(可以使用#ifndef … #endif或者#pramaonce来避免);
关于之前一直没有理解的头文件的问题这里我可以简单做一下总结,我们使用#include头文件的方式仅仅只是将头文件复制到了源文件中,假如两个源文件都#include了同一个头文件,那么这个头文件会被复制两次分别拓展到两个源文件中,当两个源文件分别编译完成后,来到链接阶段,此时它们会根据自己引用的其他模块中的符号来与其他文件进行链接(有些资料说的是链接器会无脑把所有目标文件链接在一起,这个可能得深入学习链接器才能判别真假),最终形成可执行文件;所以对于网上有些说#头文件是两个源文件的粘合剂之类的都是扯淡,头文件仅仅声明了一些必要的信息防止编译器报错或浪费时间,源文件的链接靠的是链接器;参考链接C++语言编译链接原理简介 (douban.com)
2.存储方案
C++使用四种存储方案来存储数据,这些方案的区别主要在于数据保留在内存中的时间:
- 自动存储持续性:在
函数定义中声明
的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。C++有两种存储持续性为自动的变量。 - 静态存储持续性:在
函数定义外定义
的变量和使用关键字static定义
的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。C++有3种存储持续性为静态的变量。 - 线程存储持续性:当前,多核处理器很常见,这些CPU可同时处理多个执行任务。这让程序能够将计算放在可并行处理的不同线程中。如果变量是使用``关键字thread_local声明`的,则其生命周期与所属的线程一样长。
- 动态存储持续性:用
new运算符
分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储(free store)或堆(heap)。
3.作用域和链接性
作用域描述了名称在文件的多大范围内可见:例如,函数中定义的变量可在该函数中使用,但不能在其他函数中使用:而在文件中的函数定义之前定义的变量则可在所有函数中使用。;
链接性描述了名称如何在不同单元间共享:链接性为外部的名称可在文件问共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享。;
4.名称空间
前面简单介绍过名称空间,本质上就是为了防止C++出现名称冲突;
- 声明区域:是可以在其中进行声明的区域(代码块、所在文件),每个声明区域都可以声明名称,这些名称独立于其他声明区域中声明的名称;
- 潜在作用域:变量的潜在作用域从声明点开始到其声明区域的结尾;
- 作用域:变量对程序而言可见的范围被称为作用域
C++通过定义一种新的声明区域来创建命名的名称空间(使用namespace关键字)
1 |
|
名称空间可以是全局的也可以位于另一个名称空间中,但不能位于代码块中;
通过作用域解析符可以访问给定名称空间中的名称
1 |
|
Q:我们知道,在定义类的成员函数的时候也会用到::运算符,此时该运算符应该是被重置了的,那么::一共有哪些用法呢?
A:答案参考(21条消息) c++中的::作用_保护大苹果和橙子的博客-CSDN博客
七、对象和类
在C++中由用户自定义的复合数据类型统称为类类型,不同于内置的数组、结构等复合类型,类拥有更自由的权限、可自定义其中的方法;
类规范由两个部分组成:
- 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口(头文件中实现);
- 类方法定义:描述如何实现类成员函数(对应源文件中实现);
C++中类和结构唯一的区别就是结构的默认访问类型是public而类为private;
C++程序员通常使用类来实现类描述,把结构限制为只表示纯粹的数据对象;
- C++自动将定义位于类声明中的函数成为
内联函数
,当然也可以在类声明之外定义成员函数使其成为内联函数,只需要在类实现部分定义函数时使用inline限定符即可; - 内联函数的特殊规则要求在每个使用它的文件中都对其进行定义,确保所有文件都可使用该内联函数(最简单的方法就是将内联定义放在类声明的头文件中)
创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员,但同一个类的所有对象共享同一组类方法,也就是每个方法只存在一个副本;
OOP中调用成员函数被称为发送消息,也就是说将同样的消息发送给两个不同对象将调用同一个方法,但该方法被用于两个不同的对象;
1.this指针
有些时候类成员函数并不只涉及一个对象(调用该类成员函数的对象),当方法涉及到两个对象的时候这种情况需要使用C++的this指针;
this指针指向用来调用成员函数的对象,this会被当作隐藏参数传递给方法所以不需要在方法形参列表中写出(函数sock1.top()将this设置为sock1对象的地址,函数sock2.top()将this设置为sock2对象的地址);
每个成员函数(包括构造和析构函数)都有一个this指针,如果方法需要引用整个调用对象可以使用表达式*this;
this指针主要有以下用途(参考自(19条消息) this指针的特点及用途_豪族大右的博客-CSDN博客_为什么要有this指针):
- 当形参和成员变量同名时,解决变量名冲突;
- 在类的非静态成员函数中返回对象本身,可使用return *this;
2.运算符重载
运算符重载是C++的一种多态(前面介绍的函数重载也是C++的一种多态),运算符重载将重载的概念拓展到运算符上,允许赋予C++运算符多种含义,C++根据操作数的数目和类型决定采用哪种操作;
- 运算符函数格式
1 |
|
关于C++运算符的重载还有许多限制,具体可以查看书上P387
3.友元
通常情况下C++的公有类方法提供唯一的访问类对象的私有部分的途径,C++提供了另一种形式的访问权限:友元
- 友元函数:通过让函数称为类的友元可以赋予该函数与类的成员函数相同的访问权限
- 友元类
- 友元成员函数
3.1 友元函数
一般情况下非成员函数不能直接访问类的私有数据,存在一类非常特殊的非成员函数称为友元函数,可以访问类的私有成员;
创建友元需要将其原型放在类声明
中并在原型前加关键字friend
1 |
|
- 虽然Time函数在类声明中声明,但它不是成员函数,因此不能使用成员运算符(.)或者(->)来调用,也不要在写定义的时候使用Time::限定符;
- 虽然Time不是成员函数,但是它和成员函数的访问权限相同;
- 不要在写Time()定义的时候加friend关键字;
- 只有类声明可以决定哪一个函数是友元,即类声明仍然唯一控制着可以访问私有数据的权限;
3.2 友元类
友元类的所有方法都可以访问原始类的私有成员和保护成员,当然也可以做出一些限制,选择仅让特定的类成员
成为另一个类的友元
而不必让整个类都成为友元
4.类内存
静态类成员(使用static关键字修饰)有一个特点:无论创建多少对象,程序只会创建一个静态类成员的副本即类的所有对象共享同一个静态成员,这对于具有相同值的类的私有数据的类对象是非常方便的;
- 不能在类声明中初始化静态成员变量,对于静态成员可以在类声明之外使用单独的语句进行初始化(初始化语句不需要使用关键字static但是要
使用作用域运算符指出静态成员所属类
),这是因为静态成员是单独存储的而不是对象的组成部分;
更多关于静态成员的讲解可以查看CPP · 语雀 (yuque.com)
关于类的高级用法如复制构造函数、类的动态内存分配以及对象指针等这些知识点等在实际开发过程中使用到了再学习理解;
“在类构造函数中,可以使用new为数据分配内存,然后将内存地址赋给类成员。这样,类便可以处理长度不同的字符串,而不用在类设计时人为规定数组成员(或者是结构成员)的长度。在类构造函数中使用new,也可能在对象过,期时引发问题。如果对象包含成员指针,同时它指向的内存是由new分配的,则释放用于保存对象的内存并不会自动释放对象成员指针指向的内存。因此在类构造函数中使用new类来分配内存时,应在类析构函数中使用delete来释放分配的内存。这样,当对象过期时,将自动释放其指针成员指向的内存。
如果对象包含指向new分配的内存的指针成员,则将一个对象初始化为另一个对象,或将一个对象赋给另一个对象时,会出现问题。在默认情况下,C++逐个对成员进行初始化和赋值,这意味着被初始化或被赋值的对象的成员将与原始对象完全相同。如果原始对象的成员指向一个数据块,则副本成员将指向同一个数据块。当程序最终删除这两个对象时,类的析构函数将试图删除同一个内存数据块两次;这将出错。解决方法是:定义一个特殊的复制构造函数
来重新定义初始化,并重载赋值运算符。在上述任何一种情况下,新的定义都将创建指向数据的副本,并使新对象指向这些副本。这样;旧对象和新对象都将引用独立的、相同的数据,而不会重叠。由于同样的原因;必须定义赋值运算符。对于每一种情况,最终目的都是执行深度复制,也就是说,复制实际的数据,而不仅仅是复制指向数据的指针。
”——《C++ Primer Plus》
5.继承
继承不能删除原来的基类成员函数,只能添加数据成员、成员函数或者覆盖原来的成员函数;
- 使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问;
- 派生类不能直接访问基类的私有成员,必须通过基类方法进行访问(这意味着派生类的构造函数必须使用基类的构造函数,基类构造函数负责初始化继承的数据成员,派生类构造函数主要负责初始化新增的数据成员,同理,派生类对象过期时程序首先调用派生类的析构函数再调用基类的析构函数);
- 创建派生类对象时程序首先创建基类对象(这意味着基类对象应当在程序进入派生类的构造函数之前被创建);
- 基类指针可以在不进行显式类型转换的情况下指向派生类对象:基类引用可以在不进行显式类型转换的情况下引用派生类对象:
- 基类指针或引用只能用于调用基类方法,不能调用派生类的方法(JAVA里也是这样,这是OOP的铁规距)也不可以将基类对象的地址赋值给派生类的引用和指针;
5.1 继承方式
- 公有继承:公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象;可以对基类对象执行的任何操作,也可以对派生类对象执行;
- 保护继承:关键字 protected与private相似,在类外只能用公有类成员来访问protected部分中的类成员。private和protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。
- 私有继承
关于C++的高级特性如模板、STL参考STL初级 - Tintoki_blog (gitee.io)
说明:本来打算的是好好按照《C++ Primer Plus》整理知识点,但是随着进度的推荐到书本 第十章:对象和类 之后就感觉不太能读下去了,主要是因为作者的叙述实在是太多导致文章很冗杂,光是对类和对象就介绍了7章,关键是那种深入的知识点也没怎么涉及…感兴趣愿意静下心来看一下当然是可以的,这之后我就不再做整理了,想要快速过一遍C++对象和类之后的内容可以参考B站黑马的视频,这之后还会有C++的进阶模块,主要参考《C++ Primer》和《Effective C++》;