C++
自用 c++学习笔记,正在完善,使用书籍:郑莉 c++/c++ premier
杂记
override
显式函数覆盖
声明该函数必须覆盖基类的虚函数,编译器可发现“未覆盖”错误,运用显式覆盖,编译器会检查派生类中声明override的函数,在基类中是否存在可被覆盖的虚函数,若不存在,则会报错。Only virtual member functions can be marked 'override'
struct Base {
virtual void some_func(float);
};
struct Derived : Base {
virtual void some_func(int) override; // 错误:并没有覆盖
virtual void some_func(float) override; // 正确
};
final
用来避免类被继承,或是基类的函数被覆盖
1. struct Base1 final { };
struct Derived1 : Base1 { }; // 编译错误:Base1为final,不允许被继承
2. struct Base2 {
virtual void f() final;
};
struct Derived2 : Base2 {
void f(); // 编译错误:Base2::f 为final,不允许被覆盖
};
auto
编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而要做到这一点并非那么容易,有时甚至根本做不到。为了解决这个问题, C++11 新标准引入了 auto 类型说明符,用它就能让编译器替我们去分析表达式所属的类型~(auto 让编译器通过初始值来推算变量的类型。)~
auto 定义的变量必须有初始值
使用 auto 也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i= 0,*p=&ii ;// 正确:i是整数、P是整型指针
auto sz = 0,pi = 3.14;// 错误:SZ 和pi 的类型不一致
auto val=val1+val2;//如果 val1 和 vall2 运算结果是 int 则 val 是 int 型
decltype
在某些情况下,我们定义一个变量与某一表达式的类型相同,但并不想用该表达式初始化这个变量,这时需要 decltype 变量,它的作用是选择并返回操作数的数据类型。在此过程中编译器分析表达式得到其类型,但没有实际计算表达式的值。使用decltype 时需要紧跟一个圆括号,圆括号内为一个表达式,声明的变量与该表达式类型一致。
decltype (i) j=2;//表示j以2作初始值,类型与i一致
左值和右值
-
左值是位于赋值语句左侧的对象变量,右值是位于赋值语句右侧的值,右值不依附于对象,右值是无法通过解引用获得其地址的值。
int i = 42; // i 是左值,42是右值 int foolbar(); int j = foolbar(); //foolbar()是右值
-
左值和右值之间的转换
int i = 5, j = 6; // i 和 j 是左值 int k = i + j; // 自动转化为右值表达式
-
左值引用和右值引用
-
对持久存在变量的引用称为左值引用,用&表示
-
对短暂存在可被移动的右值的引用称之为右值引用,用&&表示,右值引用就是用在移动语义中的
float n = 6;//n 为左值 持久存在 float &lr_n = n; //左值引用 float &&rr_n = n; //错误,右值引用不能绑定到左值 float &&rr_n = n * n; //右值表达式绑定到右值引用 n*n的结果是临时存在的
-
通过标准库 中的move函数可将左值对象移动为右值(有疑惑)
float n = 10; float &&rr_n = std::move(n); //将n转化为右值
在这段代码中,
std::move()
函数用于将一个左值(n
)转换为右值引用,以便将其绑定到右值引用变量rr_n
上。通过这样的转换,rr_n
现在可以将n
的资源(例如内存)“窃取”(即移动),而n
将不再保有这些资源。在使用
std::move()
之后,我们承诺除了重新赋值n
或销毁n
之外,不再使用n
,否则访问它会产生一个未指定的值(这么操作后 n 变为有效但未定义)
-
临时对象
语法(返回一个临时对象)(只有一个类的构造函数是公有时才可这么写)
return 类名(构造函数参数表);//其实是调用类的构造函数创造一个临时对象
assert
assert 的含义是“断言”,它是标准C++的
cassert
头文件中定义的一个宏,用来判断一个条件表达式的值是否为 true,如果不为true,则程序会中止,并且报告出错误,这样就很容易将错误定位。
assert (条件表达式);
一个程序一般可以以两种模式编译—-调试(debug)模式和发行(release)模式,assert 只在调试模式下生效,而在发行模式下不执行任何操作,这样兼顾了调试模式的调试需求和发行模式的效率需求。
由于 assert 只在调试模式下生效,一般用 assert 只是检查程序本身的逻辑错误,而用户的不当输入造成的错误,则应当用其他方式加以处理
main函数
main()函数的返回值是0意味着程序正常结束,如果 main()以非0值返回,则意味着程序异常结束
main 函数的返回值类型必须是 int
main 函数不能递归调用
预处理指令
在编译器对源程序进行编译之前,首先要由预处理器对程序文本进行预处理。预处理器提供了一组编译预处理指令和预处理操作符。
预处理指令实际上不是C++语言的一部分,它只是用来扩充C++程序设计的环境。
所有的预处理指令在程序中都是以“#”来引导
每一条预处理指令单独占用一行,不用分号结束。
预处理指令可以根据需要出现在程序中的任何位置。
#include
#include 被称为编译预处理指令中的文件包含指令,其作用是在指示编译器在对程序进行预处理时,将另一个源文件嵌人到当前源文件中该点处。这个被嵌人的文件可以是.h文件(通常),也同样可以是.cpp 文件。
文件包含指令 include 可以有两种书写方式。
- #include <文件名>
表示按照标准方式搜索要嵌如的文件,该文件位于编译环境的include 子目录下,一般要如人系统提供的标准文件时采用这样的方式,如对标准头文件 iostream 的包含。 - #include"文件名"
表示首先在当前目录下搜索要嵌入的文件,如果没有,再按照标准方式搜索,对用户自己编写的文件一般采用这种方式
#define和#undef 指令
预处理器最初是为C语言设计的, #define 曾经在C程序中被广泛使用,但 #define 能完成的一些功能,能够被C++引入的一些语言特性很好地替代:
- 在C语言中,用define来定义符号常量
#define PI 3.14
在C++中虽然仍可以这样定义符号常量,但是更好的方法是在类型说明语句中用const 进行修饰。 - 在C语言中,还可以用define 定义带参数宏,以实现简单的函数计算,提高程序的运行效率,
在C++中这一功能已被内联函数取代。
用define 还可以定义空符号:
#define MYHEAD_H
定义它的目的,仅仅是表示“MYHEAD_H已经定义过”这样一种状态。将该符号配合条件编译指令一起使用,可以起到一些特殊作用,这是C++程序中define 的最常用之处。
#undef 的作用是删除由 #define 定义的宏,使之不再起作用。
undef 的作用是删除由#define 定义的宏,使之不再起作用。
条件编译指令
利用条件编译可以使同一个源程序在不同的编译条件下产生不同的目标代码。
//类型 1:类比 if 语句多种情况
#if 常量表达式1
程序段1
#elif 常量表达式2
程序段2
#else
程序段3
#endif
//类型 2
#ifdef 标识符//如果“标识符”经 #defined 定义过,且未经 undef 删除,则编译程序段1
程序段1
#else
程序段2
#endif
//类型 3
#ifndef 标识符
程序段1
#else
程序段2
#endif
由于文件包含指令可以嵌套使用,在设计程序时要避免多次重复包含同一个头文件, 否则会引起变量及类的重复定义
//head.h # ifndef HEAD_H #define HEAD_H class Point { } #endif
在这个头文件中,首先判断标识符 HEAD_H 是否被定义过。若未定义过,说明此头文件尚未参加过编译,于是编译下面的程序段,并且对标识符 HEAD_H进行宏定义,标记此文件已参加过编译。若标识符 HEAD_H被定义过,说明此头文件参加过编译,于是编译器忽略下面的程序段。这样便不会造成对类 Point 的重复定义。
defined 操作符
defined是一个预处理操作符(与表达式相关),不是指令
defined 操作符使用的形式为:
defined(标识符)
若“标识符”在此前经#define 定义过,并且未经 #undef 删除,则上述表达式为非0
iostream
文件 iostream 中声明了程序所需要的输人和输出操作的有关信息。cout 和“<<”操作的有关信息就是在该文件中声明的。由于这类文件常被嵌人在程序的开始处,所以称之为头文件。在C++ 程序中如果使用了系统中提供的一些功能,就必须嵌入相关的头文件。
标准 c++库
在C语言中,系统函数、系统的外部变量和一些宏定义都放置在运行库(run-time library)中。C++ 的库中除继续保留了大部分C语言系统函数外,还加人了预定义的模板和类。标准C++类库是一个极为灵活并可扩展的可重用软件模块的集合。标准(++ 类与组件在逻辑上分为如下6种类型。
- 输入输出类
- 容器类与 ADT(抽象数据类型)
- 存储管理类
- 算法
- 错误处理
- 运行环境支持
包含这些头文件的目的是在当前编译单元中引入所需的引用性声明
C++系统函数
调用函数之前必须先加以声明,系统函数的原型声明已经全部由系统提供了,分类保存在不同的头文件中。程序员需要做的事情,就是用include 指令嵌入相应的头文件,然后便可以使用系统函数。
编译环境提供的系统函数分为两类,一类是标准C++的函数,另一类是非标准C++ 的函数,它是当前操作系统或编译环境中所特有的系统函数
随机数
c 语言中,需要cstdlib
头文件来使用下列两个函数,设置种子相同则每次运行生成的随机数是相同的
rand:求出并返回一个伪随机数
函数原型:int rand();
srand:为使rand()产生一序列伪随机整数而设置起始点。使用1作为seed参数,可以重新初化rand()。
函数原型:void srand(unsigned int seed);
#include <cstdlib>
srand(time(0));//time (0)返回从格林尼治时间1970年1月1日00:00:00到当前时刻的秒数
for(int i=1;i<=10;i++)
cout<<rand()%10<<" ";//得到范围为 0 到 9的随机数
cmath
–求平方根函数(sqrt) | ||
–求绝对值函数(abs) |
int rand(void)
产生一个伪随机数,伪随机数并不是真正随机的。这个函数自己不能产生真正的随机数。如果在程序中连续调用 rand,期望由此以产生一个随机数序列,你会发现每次运行这个程序时产生的序列都是相同的,这称为伪随机数序列。这是因为函数rand 需要一个称为“种子”的初始值,种子不同,产生的伪随机数也就不同。因此只要每次运行时给予不同的种子,然后连续调用 rand便可以产生不同的随机数序列。如果不设置种子,rand 总是默认种子1。不过设置种子的方法比较特殊,不是通过函数的参数,而是在调用它之前,需要首先调用另外一个函数 void srand (unsigned int seed)为其设置种子,其中的参数 seed 便是种子。
字符集
字符集是构成C++语言的基本元素。用C++语言编写程序时,除字符型数据外,其他所有成分都只能由字符集中的字符构成。
C++语言的字符集由下述字符构成。
-
英文字母:A ~ Z,a ~ z
-
数字字符:0~9
-
特殊字符:
词法记号
关键字
关键字是C++预先声明的单词,它们在程序中有不同的使用目的。
asm | auto | bool | break | case | catch | char |
---|---|---|---|---|---|---|
class | const | const_cast | continue | default | delete | do |
double | dynamic_cast | else | enum | explicit | export | extern |
false | float | for | friend | goto | if | inline |
int | long | mutable | namespace | new | operator | private |
protected | public | register | reinterpret_cast | return | short | signed |
sizeof | static | static_cast | struct | switch | template | this |
throw | true | try | typedef | typeid | typename | union |
unsigned | using | virtual | void | volatile | wchar_t | while |
标识符
是程序员定义的单词,它命名程序正文中的一些实体,如函数名、变量名、类名、对象名等。C++标识符的构成规则如下。
- 以大写字母、小写字母或下划线(_)开始。
- 可以由以大写字母、小写字母、下划线(_)或数字0~9组成。
- 大写字母和小写字母代表不同的标识符。
- 不能是C++关键字。
文字
在程序中直接使用符号表示的数据,包括数字、字符、字符串和布尔文字
操作符
用于实现各种运算的符号
分隔符
用于分隔各个词法记号或程序正文,C++ 分隔符是: ( )
{ }
,
:
;
这些分隔符不表示任何实际的操作,仅用于构造程序
空白
在程序编译时的词法分析阶段将程序正文分解为词法记号和空白。空白是空格、制表符(Tab键产生的字符)、垂直制表符、换行符、回车符和注释的总称。
空白符用于指示词法记号的开始和结束位置,但除了这一功能之外,其余的空白将被忽略。因此,C++程序可以不必严格地按行书写,凡是可以出现空格的地方,都可以出现换行。
注释
//:
从“//”开始,直到它所在行的行尾,所有字符都被作为注释处理。
/* code */:
“/ ”和“ /”之间的所有字符都被作为注释处理
语句
C++语言的语句包括空语句
、声明语句
、表达式语句
、选择语句
、循环语句
、跳转语句
、复合语句
、标号语句
几类。
一个独立的分号;
就构成一个空语句
,空语句不产生任何操作
将多个语句用一对大括号包围,便构成一个复合语句
C++语言没有赋值语句也没有函数调用语句,赋值与函数调用功能都是通过表达式来实现的
如果在赋值表达式后面加上分号,便成为了表达式语句
表达式与表达式语句的不同点:一个表达式可以作为另一个更复杂表达式的一部分,继续参与运算;而语句则不能
基本数据类型
类型名 | 长度(字节) | 取值范围(根据编译器不同) |
---|---|---|
bool | 1 | true 或 false |
char | 1 | -128 到 127 或 0 到 255 |
int(sign) | 4 | -2147483648 到 2147483647 |
short(sign) | 2 | -32768 到 32767 |
long(sign) | 4 | -2147483648 到 2147483647 |
float | 4 | 3.4×10^-38^~3.4×10^38^ |
double | 8 | 1.7×10^-308^~1.7×10^308^ |
long double | 8、10、12 、 16 | 在用 8 字节实现时完全同 double 其他字节实现时精度更准确 |
wchar_t | 2 或 4 | 0 到 65535 或 -2147483648 到 2147483647 |
char16_t | 2 | 0 到 65535 |
char32_t | 4 | 0 到 4294967295 |
long long | 8 | -9223372036854775808 到 9223372036854775807 |
关键字 signed 和 unsigned,以及关键字 short 和long 被称为修饰符
char 型从本质上说也是整数类型,它是长度为1个字节的整数,通常用来存放字符的 ASCII 码
用 short 修饰int 时,short int 表示短整型,占2字节。此时 int 可以省略,因此表2-1 中列出的是 short 型而不是short int 型。long 可以用来修饰 int 和double。用long 修饰int 时,long int 表示长整型,占4字节,同样此时 int也可以省略。
多数编译系统中 unsigned 表示4个字节的无符号整数
细节:
ISO C++标准并没有明确规定每种数据类型的字节数和取值范围,它只是规定它们之间的字节数大小顺序满足:
(signed/unsigned) char≤ (unsigned) short≤ (unsigned)int≤ (unsigned) long
不同的编译器对此会有不同的实现。面向32 位处理器(IA-32)的C++编译器通常将 int 和long 两种数据类型皆用4个字节表示,但一些面向64位处理器(IA-64 或x86-64)的C++编译器中,int 用4个字节表示,long 用8个字节表示。因此(unsigned)int 和(unsigned)long虽然在表中具有相同的取值范围,但仍然是两种不同的类型。
一般情况下,如果对一个整数所占字节数和取值范围没有特殊要求,使用(unsigned) int 型为宜,因为它通常具有最高的处理效率。
signed 和 unsigned 可以用来修饰char 型和 int 型(也包括short和 long),signed 表示有符号数,unsigned 表示无符号数。有符号整数在计算机内是以二进制补码形式存储的,其最高位为符号位,“0”表示“正”,“1”表示“负”。无符号整数只能是正数,在计算机内是以绝对值形式存放的。int 型(也包括 short 和long)在默认(不加修饰)情况下是有符号(signed)的。
细节
char 与 int,short,long 有所不同,ISO C++ 标准并没有规定它在默认(不加修饰)情况下是有符号的还是无符号的,它会因不同的编译环境而异。因此char,signed char 和 unsigned char 是3种不同的数据类型。
两种浮点类型除了取值范围有所不同外,精度也有所不同,float 可以保存7位有效数字,double 可以保存15 位有效数字。
bool(布尔型,也称逻辑型)数据的取值只能是false(假)或true(真)。bool 型数据所占的字节数在不同的编译系统中有可能不一样,在 VC++.NET 2005 编译环境中 bool型数据占1字节。
常量
在程序运行的整个过程中其值始终不可改变的量
直接使用符号(文字)表示的值。例如:12,3.5,A'都是常量。
整形常量
文字形式出现的整数(包括 0)
表示形式
- 十进制:(「 ± 」+ 「若干0 到 9 之间的数字」)
+9
- 八进制:以数字 0 开头(0若干个0~7的数字)
086
- 十六进制:以数字 0x 开头(0x若干个 0 ~ 9的数字及A ~ F的字母(大小写均可))
0xa4f
- 整形常量后加 L( l )表长整形,U(u)表无符号,都加都表示
由于八进制和十六进制形式的整型常量一般用来表示无符号整数,所以前面不应带正负号。
实型常量
文字形式出现的实数,例如12.5
一12.5
0.345E+2
等。
实型常量默认为 double 型,后缀F(或f)可以使其成为float型
指数形式:例如0.345E+2
表示 0.345×10^2^。(字母E可以大写或小写)当以指数形式表示一个实数时,整数部分和小数部分可以省略其一,但不能都略。例如:.123E-1
12.E 2
1.E—3
都是正确的,但不能写成E-3
这种形式。
字符常量
是单引号括起来的一个字符,如:‘D’
‘$’
等。
无法通过键盘输入的不可显示字符通过使用转义字符表示
无论是不可显示字符还是一般字符,都可以用十六进制或八进制 ASCII码来表示, 表示形式是:
\nnn
八进制形式
\xnnn
十六进制形式
例如,‘a'
的十六进制 ASCII 码是61
,于是a也可以表示为\x61
。
字符数据在内存中以 ASCII码的形式存储,每个字符占1字节,使用7个二进制位。
字符串常量(字符串)
用一对双引号括起来的字符序列:“ABCD”
"this is a string"
字符串中间的双引号就要用转义序列来表示:"Please enter \"Yes\" or \"No\""
内存中存放形式:按串中字符的排列次序顺序存放,每个字符占一个字节,并在末尾添加'\0'
作为结尾标记。
字符串常量实际上是一个隐含创建的类型为 char 的数组,一个字符串常量就表示这样一个数组的首地址。因此,可以把字符串常量赋给指向常量的字符串指针const char * string1= "This is a string.";
布尔常量
只有两个:false(假)和 true(真)
符号常量
为常量命名
符号常量在使用之前一定要首先声明,在声明时一定要赋初值,而在程序中间不能改变其值
1. const 数据类型说明符 常量名=常量值;
2. 数据类型说明符 const 常量名=常量值;
变量
在程序的执行过程中其值可以变化的量称为变量
变量是需要用名字(标识符)来标识的
变量声明与定义
变量在使用之前需要首先声明其类型和名称(标识符)
数据类型 变量名1,变量名2,⋯,变量名n;
声明一个变量只是将变量名标识符的有关信息告诉编译器,使编译器“认识”该标识符,但是声明并不一定引起内存的分配。
定义一个变量意味着给变量分配内存空间,用于存放对应类型的数据,变量名就是对相应内存单元的命名
在C++ 程序中,大多数情况下变量声明也就是变量定义,声明变量的同时也就完成了变量的定义(即有空间),只有声明外部变量时例外。
在定义一个变量的同时,也可以给它赋予初值,而这实质上就是给对应的内存单元赋值,有多种给基本类型变量赋初值的方式
1. int a = 0;
2. int a(0);
3. int a = {0};
4. int a{0};
//其中使用大括号的初始化方式称为列表初始化,列表初始化时不允许信息的丢失。例如用double值初始化int变量,就会造成数据丢失。
变量的存储类型(不同于数据类型)
决定了其存储方式
auto 存储类型:采用堆栈方式分配内存空间,属于暂时性存储,其存储空间可以被若干变量多次覆盖使用。
register 存储类型:存放在通用寄存器中。
extern 存储类型:在所有函数和程序段中都可引用。
static 存储类型:在内存中是以固定地址存放的,在整个程序运行期间都有效。
全局变量和局部变量
全局变量:变量的定义全部写在函数以外,分配唯一确定的地址
局部变量:变量的定义都放在一个函数之内,存储在运行栈中。
运行栈:是一段区域的内存空间,与存储全局变量的空间无异,只是寻址的方式不同而已。运行栈中的数据分为一个一个栈帧,每个栈帧对应一次函数调用,栈帧中包括这次函数调用中的形参值、一些控制信息、局部变量值和一些临时数据(例如复杂表达式计算的中间值、某些函数的返回值),每次发生函数调用时,都会有一个栈帧被压入运行栈中,而调用返回后,相应的栈帧会被弹出。
一个函数在执行过程中能够直接随机访问它所对应的栈帧中的数据,即处在运行栈最顶端的栈帧的数据(执行中的函数的栈帧,总处在运行栈的最顶端)。当一个函数调用其他函数时,要为它所调用的函数设置实参,具体方式是在调用前把实参值压入栈中,运行栈中的这一部分空间是主调函数与被调函数都可以直接访问的,参数的形实结合就是通过访问这一部分公共空间完成的。虽然一个函数在被调用时的形参和局部变量地址是不确定的,但它们的地址相对于栈顶地址却是确定的,这样就可以通过栈顶的地址,定位形参和局部变量。
变量的实现机制
在C++源程序中,之所以要使用变量名,是为了把不同的变量区别开。在运行程序时,C++变量的值都存储在内存中。内存中的每个单元都有一个唯一的编号,这个编号就是它的地址。不同的内存单元的地址互不相同,因此不同名称的变量在运行时占据的内存单元具有互不相同的地址,C++的目标代码就是靠地址来区别不同的变量。
自定义数据类型
枚举类型、结构类型、联合类型、数组类型、类类型
类型别名
typedef 已有类型名 新类型名表//旧
using 新类型名=已有类型名//新
enum 枚举
c++分限定作用域的枚举和不限定作用域的枚举
不限定作用域的枚举
/*枚举类型声明*/
enum Weekday
{ sun=9,mon=1,tue,wed,thu=7,fri,sat };//注意分号
/*枚举类型实例*/
Weekday test=sun;
cout<<sun<<' '<<mon<<' '<<tue<<' '<<wed<<' '<<thu<<' '<<fri<<' '<<sat<<endl;
//运行结果 注意没初始化赋值的枚举类型的值
9 1 2 3 7 8 9
在枚举类的作用领域内可以直接使用枚举类中的标识符,若重新声明了该标识符,则使用的是重新声明的标识符
对枚举元素按常量(整数)处理,不能对它们赋值
枚举值全部会在编译时计算出来
枚举元素具有默认值,它们依次为:0,1,2,⋯
枚举值可以进行关系运算
枚举类型的数据可以和整型数据相互转換,枚举型数据可以隐含转换为整型数据,整型数据到枚举数据的转换,则需要采用显式转换的方式(强制类型转换)
C++ 中声明完枚举类型后,声明变量时,可以不写关键字 enum
匿名枚举
通过匿名枚举来达到定义 常量的目的
enum {
内容
}
其他地方直接使用枚举常量名,当整形常量来使用
限定作用域的枚举
在限定作用域的枚举类型中,枚举成员的名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的
限定作用域的枚举类型不会进行隐式转换为 int 型
枚举类型前置声明
enum intValues :unsigned long long; // 不限定作用域的,必须指定成员类型
enum Class open_modes;//限定作用域的枚举类型可以使用默认成员类型 int
指定enum 的大小
尽管每个 enum 都定义了唯一的类型,但实际上 enum 是由某种整数类型表示的。在C++11 新标准中,我们可以在 enum 的名字后加上冒号以及我们想在该 enum 中使用的类型:
enum intValues : unsigned long long {
charTyp = 255, shortTyp = 65535, intTyp = 65535,
longTyp = 4294967295UL,
long_longTyp = 18446744073709551615ULL
};
如果我们没有指定 enum 的潜在类型,则默认情况下限定作用域的enum 成员类型是int。对于不限定作用域的枚举类型来说,其枚举成员不存在默认类型,我们只知道成员的潜在类型足够大,肯定能够容纳枚举值。如果我们指定了枚举成员的潜在类型(包括对限定作用域的 enum 的隐式指定),则一旦某个枚举成员的值超出了该类型所能容纳的范围,将引发程序错误。
运算符和表达式
我们可以简单地将表达式理解为用于计算的公式,它由运算符(例如:+
,-
,*
,/
)、运算量(也称**操作数,可以是常量、变量等)和括号组成。执行表达式所规定的运算,所得到的结果值便是表达式的值。例如:a+b
x/y
都是表达式。
表达式定义:**
- 一个常量或标识对象的标识符是一个最简单的表达式,其值是常量或对象的值。
- 一个表达式的值可以用作其他运算符的操作数
- 包含在括号中的表达式仍是一个表达式,其类型和值与未加括号时的表达式相同。
有些运算符需要两个操作数,使用形式为: 操作数1 运算符 操作数2
这样的运算符称为二元运算符(或二目运算符)。另一些运算符只需要一个操作数,称为一元运算符(或单目运算符)。
算术运算符和算数表达式
C++ 中的算术运算符包括基本算术运算符和自增自减运算符。由算术运算符、操作数和括号构成的表达式称为算术表达式
“+
”作为正号、“-
”作为负号时为一元运算符,其余都为二元运算符
“%
”是取余运算,只能用于整型操作数,表达式a%b的结果是a被b除的余数。若两者符号不同,是两者绝对值取余后再取 a 的符号作为最终值,0%任何数都为 0
当”/”用于两个整型数据相除时
其结果取商的整数部分,小数部分被自动舍弃。因此,表达式1/2的结果为0
++(自增)、--(自减)运算符 都是将操作数的值增1(减1)后,重新写回该操作数在内存中原有的位置,但是,当自增、自减运算的结果要被用来继续参与其他操作时,前置与后置时的情况就完全不同了
赋值运算符和赋值表达式
带有赋值运算符(如=
)的表达式被称为赋值表达式
赋值表达式的作用:将等号右边表达式的值赋给等号左边的对象。
赋值表达式的类型:等号左边对象的类型,其结果值为等号左边对象被赋值后的值,
赋值表达式运算的结合性:自右向左
a+=a-=a*a
等价于a=a+(a=a-a*a)
逗号运算和逗号表达式
表达式1,表达式2,...,表达式n
求解顺序:从左到右依此求解 最终结果:最后一个表达式的值
a=3*5,a*4
//表达式 1 为a=3*5,表达式 2 为a*4
//最终结果为 60
int a=3,b=4; a=b++,b;
最终结果为a=4,b=5
逻辑运算和逻辑表达式
关系运算符: 用子比较、判断
优先次序为:
用关系运算符将两个表达式连接起来,就是关系表达式,关系表达式的结果类型为 bool
,值只能为 true
或false
若只有简单的关系比较是远不能满足编程需要的,我们还需要用逻辑运算符将简单的关系表达式连接起来,构成较复杂的逻辑表达式。**逻辑表达式的结果类型为bool
,值只能为true
或false
。
C++ 中的逻辑运算符及其优先次序为:
“
&&
”和“||
”运算符具有“短路”特性。对于“
&&
”,运行时先对第一个操作数求值,如果其值为 false,则不再对第二个操作数求值。因为这时无论第二个操作数的值是多少,“&&”表达式的值都是 false;对于“
||
”,运行时先对第一个操作数求值,如果其值为 true,则不再对第二个操作数求值。
条件运算符和条件表达式
表达式1? 表达式 2:表达式 3
表达式 1
必须是 bool
类型,其他的可为任何类型,且类型可以不同。
条件表达式的最终类型:2和3中较高的类型
条件表达式的执行顺序:先求解表达式1。若表达式1的值为true,则求解表达式2,表达式2的值为最终结果;若表达式1的值为false,则求解表达式3,表达式3的值为最终结果。注意,条件运算符优级高于赋值运算符,低于逻辑运算符。结合方向为自右向左。
sizeof 运算符
sizeof(类型名)
用于计算某种类型的对象在内存中所占的字节数。
运算结果值为“类型名”所指定的类型或“表达式”的结果类型所占的字节数。
注意在这个计算过程中,并不对括号中的表达式本身求值。
位运算
C++ 中提供了6个位运算符,可以对整数进行位操作(包括 char 类型)
&
:按位与
^
:按位异或
|
:按位或
移位
二元运算符<<
和>>
左边的操作数是需要移位的数值,右边的操作数是左移或右移的位数。
左移:按照指定的位数将一个数的二进制值向左移位。左移后,低位补0,移出的高位舍弃。
右移:按照指定的位数将一个数的二进制值向右移位。右移后,移出的低位舍弃。如果是无符号数则高位补0;如果是有符号数,则高位补符号位或补0,对这一问题不同的系统可能有不同的处理方法。**
**==移位运算的结果:位运算表达式(a>>2
或2<<1
)的值,移位运算符左边的表达式(变量a
或常量2
)值本身并不会被改变。
运算符优先级
优先级 | 运算符 | 结合性 |
---|---|---|
1 | [] ()函数调用 .结构体和union成员调用 -> 后置++ 后置-- |
左—>右 |
2 | 前置++ 前置-- sizeof & * +(正号) -(负号) ~ ! |
右—>左 |
3 | (强制转换类型) |
右—>左 |
4 | .* ->* |
左—>右 |
5 | * / % |
左—>右 |
6 | + - |
左—>右 |
7 | << >> |
左—>右 |
8 | < > <= >= |
左—>右 |
9 | == != |
左—>右 |
10 | & |
左—>右 |
11 | ^ |
左—>右 |
12 | | |
左—>右 |
13 | && |
左—>右 |
14 | || |
左—>右 |
15 | ?: |
右—>左 |
16 | = *= /= %= += -= <<= >>= &= ^= |= |
右—>左 |
17 | , |
左—>右 |
优先级 | 运算符 | 描述 | 结合性 |
---|---|---|---|
1 | :: |
左—>右 |
|
2 | 后置++(--) |
||
type() type{} |
|||
a() |
|||
a[] |
|||
. -> |
|||
3 | 前置++(--) +(-)a ! ~ (type)c类型强制转换运算符 *a &a sizeof() new new[] delete delete[] |
右—>左 |
|
co_await |
|||
.* ->* |
左—>右 |
||
* / % |
|||
+ - |
|||
<< >> |
|||
<=> |
|||
< <= > >= |
|||
== != |
|||
a&b |
|||
^ |
|||
| |
|||
&& |
|||
|| |
|||
a?b:c = += -= *= /= %= <<= >>= &= ^= |= |
|||
throw co_yield | |||
, |
|||
混合运算时数据类型的转换
隐含转换
算术运算符、关系运算符、逻辑运算符、位运算符和赋值运算符这些二元运算符, 要求两个操作数的类型一致。在算术运算和关系运算中如果参与运算的操作数类型不一致,编译系统会自动对数据进行转换(即隐含转换)。
转换的基本原则:低—>高。类型越高,数据的表示范围越大,精度也越高,各种类型的高低顺序如下:
逻辑运算符要求参与运算的操作数必须是 bool 型,如果操作数是其他类型,编译系统会自动将其转换为 bool型。转换方法是:非0数据转换为 true,0转换 false。
位运算的操作数必须是整数,当二元位运算的操作数是不同类型的整数时,编译系统也会自动进行类型转换
赋值运算要求左值(赋值运算符左边的值)与右值的类型相同,若类型不同,编译系统会自动进行类型转换。但这时的转换不适用表2-5中列出的隐含转换的规则,而是一律将右值转换为左值的类型
转换是安全的**:转换中没有精度损失
显式转换(强制类型转换)
将表达式的结果类型转换为另一种指定的类型
6 种类型:
-
类型说明符(表达式)
//C++风格的显式转换符号 -
(类型说明符)表达式
//C语言风格的显式转换符号 -
const _cast<类型说明符>(表达式)
-
dynamic_cast<类型说明符>(表达式)
-
reinterpret_cast<类型说明符>(表达式)
-
static_cast<类型说明符>(表达式)
使用显式类型转换注意事项。
这种转换可能是不安全的。
从上面的例子中可以看到,将高类型数据转换低类型时,数据精度会受到损失。
这种转换是暂时的、一次性的。
float z=7.56; int a; a=int (z);
强制类型转换
int(z)
只是将float型变量z的值取出来,临时转换为 int 型,然后赋给 int型的 a。这时变量z所在的内存单元中的值并未真正改变,因此再次使用z时,用的仍是原来的浮点类型值。
强制类型转换是有一定风险的,有的转换并不一定安全,如把整型数值转换成指针,把基类指针转换成派生类指针,把一种函数指针转换成另一种函数指针,把常量指针转换成非常量指针等。
static_cast
static_cast 用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型之间的互相转换,但没有运行时类型检查来保证转换的安全性。另外,如果对象所属的类重载了强制类型转换运算符 T(如 T 是 int、int* 或其他类型名),则 static_cast 也能用来进行对象到 T 类型的转换。
将派生类指针转换为基类指针等。
static_cast 不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。因为这些属于风险比较高的转换。
reinterpret_cast
int i=2;
float* p=reinterpret_cast<float*> (&i);
reinterpret_cast 是和 static_cast 并列的一种类型转换操作符,它可以将一种类型的指针转换为另一种类型的指针,这里把int * 类型的&i转换为float 类型。
这种转换提供了很强的灵活性和底层的操作等级,但转换的安全性只能由程序员的细心来保证了。
- 指针类型之间的转换:可以将一个指针转换为另一个类型的指针,但是转换后的指针不能用于间接访问其指向的对象,除非目标类型是原始字符类型或
std::byte
。 - **引用类型之间的转换:可以将一个引用转换为另一个类型的引用,但是转换后的引用可能会导致未定义的行为,除非目标类型是原始字符类型或
std::byte
。 - 指针和整数类型之间的转换**:可以将一个指针转换为整数类型,或将一个整数类型转换为指针。这样的转换通常会导致指针的信息丢失,因此需要非常小心。
这个转换是怎么进行的呢?无论是int 类型的指针,还是float 类型的指针,存储的都是一个地址,它们的区别只是相应地址中的数据被解释为不同类型而已。因此,这里的类型转换,无外乎就是把&i得到的地址值直接作为转换结果,并为这一结果赋予 float类型。这种转换的结果就是,p作为浮点型指针,却指向了整型变量i。这能不出问题吗?
reinterpret_cast 可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针。
细节
reinterpret_cast 不仅可以在不同类型对象的指针之间转换,还可以在不同类型函数的指针之间、不同类数据成员的指针之间、不同类函数成员的指针之间、不同类型的引用之间相互转换。
reinterprct_cast 的转换过程,在C++标准中未明确规定,会因编译环境而异
C++ 标准只保证用 reinterpret_cast 操作符将 A 类型的p转换B类型q, 再用 reinterpret_cast 操作符将B类型的q转换为A 类型的r后,应当有(pr)成立。
reinterpret_cast 所做的转换,一般只用于帮助实现一些非常底层的操作,在绝大多数情况下,用 rcinterpret_cast 在不同类型的指针之间转换的行为,都是应当避免的。C++ 之所以要将 reinterpret_ cast 所能执行的转换操作和 static _cast 分开,就是因为reinterpret_cast 具有很大的危险性和不确定性,而 static_cast 大部分情况下是安全的和确定的(如果用 static_cast 将 void 指针转换为指针原来的类型,那么这是一种安全的转换, 否则仍然是不安全的)
const_cast
const_cast
是用于去除变量的 const
或 volatile
修饰符的操作符。将常指针转换为普通指针,将常引用转换为普通引用。但是需要注意使用 const_cast
来修改原本被声明为 const
的对象是一种未定义行为。
void foo (const int * cp) {
int * p= const_cast< int * > (cp) ;
(* p) + + ;
}
//该代码使用 const_cast 将cp 类型中的 const 去除,将常指针cp 转换为普通指针 p,然后通过 p 修改它所指向的变量。
注意
const_cast 只用于将常指针转换为普通指针,将常引用转换为普通引用,而不用来将常对象转换为普通对象,因为这是没有意义的。因为对象(而非引用)的转换会生成对象的副本,而使用常对象本来就可以直接生成普通对象的副本
const int i=5; int j=i;
这里把常量i赋给变量j是无须任何转换的。
不过常对象可以用 const_cast 转换为普通引用,这是因为从对象到引用的转换是隐含的,常对象可以隐含地转换为常引用,而常引用又可用 const_cast 转换为普通引用。
dynamic_cast
dynamic_cast
是用于进行动态类型转换的操作符。它主要用于在运行时执行安全的向下转型(多态基类的指针强制转换为派生类的指针)
只能用于具有虚函数的类层次结构中
dynamic_cast 是通过“运行时类型检查”来保证安全性的。dynamic_cast 不能用于将非多态基类的指针或引用强制转换为派生类的指针或引用——这种转换没法保证安全性,只好用 reinterpret_cast 来完成。
- 安全地向下转型: 当需要在继承体系中将基类指针或引用转换为派生类指针或引用时,
dynamic_cast
可以在运行时检查实际对象的类型,并确保转换是安全的检查指针(或引用)所指向对象的实际类型是否与转换的目的类型兼容,如果兼容转换才会发生,才能得到派生类的指针(或引用)如果转换是不安全的(例如,试图将一个基类指针指向一个不是其派生类的对象),dynamic_cast
会返回一个空指针(指针类型的转换)或引发一个std::bad_cast
异常(如果转换的目标是引用)。 - 用于多态类型: 当基类中有虚函数,并且在派生类中重写这些虚函数时,
dynamic_cast
可以在运行时找到正确的派生类类型。这对于实现运行时多态性非常有用。 - 用于识别对象类型: 如果在程序中需要识别对象的实际类型,
dynamic_cast
可以帮助实现这一目的。这在某些情况下可能很有用,例如在处理基类指针的容器时。
细节
当原始类型为多态类型的指针时,目的类型除了是派生类指针外,还可以是void指针,例如
dynamic_cast<void * >(p);
这时所执行的实际操作是,先将P指针转换为它所指向的对象的实际类型的指针,再将其转换为void 指针。换句话说,就是得到p 所指向对象的首地址(在多继承存在的情况下,基类指针存储的地址未必是对象的首地址)。
零扩展和符号扩展(短字节类型转换成长字节类型)
short 和 unsigned short 类型的变量都占用2个字节,从这两个类型到int 类型的转换,这里所执行的是不同的操作。
-
有符号的a转换 int 类型
将a 的16位字节复制到c 的低16位,然后用a的
符号位1
填充c的高16 位,(这种用符号位填充高位的操作叫做“符号扩展”); -
无符号的b 转换为 int 类型
需要将b的低16 位字节复制到d的低16位,然后用
0
填充d的高16位 (这种用0填充高位的操作叫做“零扩展”)。
细节
由于有些CPU并没有符号扩展的功能,C++标准也允许short到更长整数的转换采取零扩展的方式来实现,但对于硬件实现了符号扩展功能的目标处理器,一般都采用符号扩展。
CPU所执行的指令并不对操作数的类型加以区分,对各个操作数都执行相同的操作。编译器需要根据变量的数据类型选择适当的指令。
函数
主调函数:调用其他函数的函数
被调函数:被其他函数调用的函数
函数定义的语法形式
类型说明符 函数名(含类型说明的形式参数表)
{
语句序列
}
形式参数
形式:type1 name1,type2 name2, …
type1,type2...是类型标识符,表示形参的类型。name1,name2...是形参名。
形参的作用是实现主调函数与被调函数之间的联系。通常将函数所处理的数据、影响函数功能的因素或者函数处理的结果作为形参。
调用时实参与形参的结合次序是从左向右
函数在没有被调用的时候是静止的,此时的形参只是一个符号,它标志着在形参出现的位置应该有一个什么类型的数据。函数在被调用时才执行,也是在被调用时才由主调函数将实际参数(简称实参)赋予形参。
main 的形式参数(也称命令行参数)
由操作系统在启动程序时初始化
main函数的参数,不管是书写的整数还是浮点数,全部被认为是字符串
int main(int argc, char *argv[],char *envp[]) {
}
第一个参数:argc是个整型变量,表示命令行参数的个数(含第一个参数),默认为1,本文的代码设置了3个参数,argc为4。
第二个参数:argv是个字符指针的数组,每个元素是一个字符指针,指向一个字符串。这些字符串就是命令行中的每一个参数(字串),argv[0]指向输入的程序路径及名称,从argv[1]开始对应相应的命令行参数。
第三个参数:envp是字符指针的数组,数组的每一个原元素是一个指向一个环境变量(字符串)的字符指针。
含有可变形参的函数
有时我们无法提前预知应该向函数传递几个实参。
为了编写能处理不同数量实参的函数,C++11 新标准提供了两种主要的方法
- 如果所有的实参类型相同,可以传递一个名为
initializer_list
的标准库类型- 如果实参的类型不同,我们可以编写可变参数模板(仅使用在用 c++读取 c 程序,在此不介绍)
initializer_list 形参
**头文件:
initializer_list
使用在函数的实参数量未知但是全部实参的类型都相同
initializer_list 对象中的元素永远是常量值
initializer_list提供的操作 | |
---|---|
initializer_list Ist; |
默认初始化:T类型元素的空列表 |
initializer_list Ist{a, b, c...} | lst 的元素数量和初始值一样多;lst 的元素是对应初始值的副本;列表中的元素是 const,无法更改 |
lst2(Ist) | 拷贝或赋值一个 initializer原始列表和副本共享元素list 对象不会拷贝列表中的元素拷贝后,原始列表和副本共享元素 |
lst2=lst | |
Ist.size() | 列表中的元素数量 |
Ist.begin() | 返回指向 lst 中首元素的指针 |
Ist.end() | 返回指向 lst 中尾元素下一位置的指针 |
#include <initializer_list>
void error_msg (initializer_list<string> il)
{
for (auto beg = il. begin (); beg != il. end () ; ++beg)//beg是一种类型的指针 自动检测
cout << *beg << " " ;
cout <<endl;
}
int main(){
string expected,actual;
if (expected != actual)
error_msg ({"function", expected, actual}) ;//如果想向 initializer_list 形参中传递一个值的序列,则必须把序列放在一对花括号内
else
error_msg ({"functionX", "okay"}) ;
}
返回值和返回值类型
函数可以有一个返回值,函数的返回值是需要返回给主调函数的处理结果。类型说明符规定了函数返回值的类型。函数的返回值由 return 语句给出,格式如下:
return 表达式;
一个函数也可以不将任何值返回给主调函数,这时它的类型标识符为void,可以不写return语句,但也可以写一个不带表达式的 return 语句,用于结束当前函数的调用,格式如下:
return;
对象作为函数参数和返回值的传递方式
在函数调用时,把对象作为参数传递,需要调用复制构造函数,但这些工作具体是如何做的呢?
函数调用时传递基本类型的数据是通过运行栈,传递对象也一样是通过运行栈。运行栈中,在主调函效和被调函数之间,有一块二者都要访问的公共区域,主调函数把实参值写人其中,函数调用发生后,被调函数通过读取这段区域就可得到形参值。需要传递的对象,只要建立在运行栈的这段区域上即可。
传递基本类型数据与传递对象的不同之处在于,将实参值复制到这段区域上时,对于基本数据类型的参数,做一般的内存写操作即可,但对于对象参数,则需要调用复制构造函数。
void fun1(point p){...}//point 是类名 //调用 int main(){ point b; fun1(b); }
调用它时,就需要调用 point的复制构造函数,使用对象b在运行栈的传参区域上构造一个临时对象。这个对象在主调函数 main 中无名,但却在被调函数 fun1中有名(就是fun1函数的参数p)。 在main 中虽然无名,但地址却可以计算,因此编译器能够生成代码调用 point 的复制构造函效,为这个对象初始化。对象参数的复制构造函数的调用在跳转到 fun1 函数的入口地址之前完成。
有时传递对象参数时,编译器会做出适当优化,使得复制构造函数不必被调用 例如:使用 Point 型的临时对象作为fun1函数的参数,对它进行调用:
point(int a){ } //point的构造函数 fun1 (Point (1));//Point (1)是构建临时对象的语法
最直接的做法是,先构造一个 Point类型的临时对象
Point(1)
,再以这个对象为参数调用复制构造函数(复制给 fun1 的参数point p
)在运行栈的传参区域上再生成一个临时对象,再执行fun1 函数的代码。但是,构造两个临时对象有一点多余。更好的做法是,直接使用Point类的构造函数,在运行栈的传参区域上建立临时对象,这样就免去了一次复制构造函数的调用(如下图)
如果在传参时发生由构造函数所定义的类型转换,复制构造函数的调用同样可以避免。例如,如果使用下面的代码调用fun1函数:
fun1(1);
fun1函数接收 Point 类型的参数,因此这需要执行从 int 型到 Point 型的隐含类型转换,而类型转换的本质是调用 Point 的构造函数来创建临时对象。由于该临时对象同样可以直接建立在运行栈的传参区域上,因此也无须再调用一次复制构造函数。
返回一个对象时,返回值的传递方式(暂且放置不整理)
传递返回值, 需要创建无名的临时对象,但是这个对象具体的创建过程是怎样的呢?
由于主调函数需要获得返回值,所以这个临时对象需要创建在主调函数的栈帧上,那么被调函数如何影响主调函数所创建的临时对象的值呢? 现在比较通行的处理方式是,由主调函数决定临时对象的创建位置,然后把临时对象的地址作为参数传递给被调函数。下面的代码模拟了过程
例4-2
//4_2.cpp
#include <iostream>
using namespace std;
class Point { //Point 类的定义
public: //外部接口
Point(int xx = 0, int yy = 0) { //构造函数
x = xx;
y = yy;
}
Point(Point &p); //复制构造函数
int getX() {
return x;
}
int getY() {
return y;
}
private: //私有数据
int x, y;
};
//成员函数的实现
Point::Point(Point &p) {
x = p.x;
y = p.y;
cout << "Calling the copy constructor" << endl;
}
//形参为Point类对象的函数
void fun1(Point p) {
cout << p.getX() << endl;
}
//返回值为Point类对象的函数
Point fun2() {
Point a(1, 2);
return a;
}
//主程序
int main() {
Point a(4, 5); //第一个对象A
Point b = a; //情况一,用A初始化B。第一次调用复制构造函数
cout << b.getX() << endl;
fun1(b); //情况二,对象B作为fun1的实参。第二次调用复制构造函数
b = fun2(); //情况三,函数的返回值是类对象,函数返回时,调用复制构造函数
cout << b.getX() << endl;
return 0;
}
为保存返回值所生成的临时对象,它的空间分配和构造函数执行这两步是分开的:
空间分配在主调函数中进行,构造函数在被调函数中执行。
返回时对复制构造函数的调用,指的就是调用复制构造函数为这个临时对象初始化,但有时这个复制构造函数的调用也是可以省去的,例如,如果把fun2 改写为:
Point fun2( ){ return Point (1,2); }
最直接的实现方式是,先调用 Point 的构造函数 Point(int,int)生成一个fun2 内的临时对象,再以这个临时对象为参数调用 Point 的复制构造函数,生成返回值,这两步可以简化为一步,即用构造函数 Point(int,int)直接构造出返回值,就是下面这样的形式:
*void _fun2 (Point &result){ result.Point(1,2); }*
(假代码,表示成斜体2,只是为了说明问题)
另一方面,在主调函数中,也未必一定要为返回值生成临时对象
例如,如果主调函数是这样调用 fun2 的:
Point p=fun2();
这时不必为返回值生成临时对象,而可以直接用对象P的空间存储返回值,就是这样(同上假代码)
*Point p;*
//这里只分配空间,不调用构造函数
*_fun2(p);*
//p的构造函数会在_fun2函数中被调用
这能够省去一次复制构造函数的调用。
复制构造函数的调用次数,会因编译器的优化程度而有所差异,因此复制构造函数中一定要只完成对象复制的任务,而不要有其他能产生副作用的操作,否则程序运行结果会因编译器的优化程度而有所差异。
函数的调用
函数在调用之前也需要声明(原型) 。若直接定义函数就相当于也声明了函数,因此,在定义了一个函数之后,可以直接调用这个函数。但如果希望在定义一个函数前调用它,则需要在调用函数之前添加该函数的函数原型声明(即函数声明) 。
函数原型声明的形式如下:
类型说明符 函数名(含类型说明的形参表);
与变量的声明和定义类似,声明一个函数只是将函数的有关信息(函数名、参数表、返回值类型等)告诉编译器,此时并不产生任何代码;定义一个函数时除了同样要给出函数的有关信息外,主要是要写出函数的代码
细节
声明函数时,形参表只要包含完整的类型信息即可,形参名可以省略,也就是说,原型声明的形参表可以按照下面的格式书写:typel1,type2,…typen(不推荐)
-
如果是在所有函数之前声明了函数原型
该函数原型在本程序文件中任何地方都有效(在本程序文件中任何地方都可以依照该原型调用相应的函数)
-
如果是在某个主调函数内部声明了被调函数原型
该原型就只能在这个函数内部有效
声明了函数原型之后,便可以按如下形式调用函数:
函数名(实参列表)