c++ primer笔记

文章目录

第一部分 c++基础

2.0 变量和基本类型

2.1 算术类型 (arithmetic type)

算术类型的尺寸在不同的电脑上有所差别,所以c++标准就列出每个类型最小的值
在这里插入图片描述
一个char因该存放机器基本字符集中任意字符对应的数字值,也就是说一个char的大小和一个机器字节一样
long double常常用来当成有特殊需求的硬件,他的具体实现不同,精度也不同
出去布尔类型和扩展的字符类型之外,其他的整型可以划分成带符号的和不带符号的,通过前面加上unsigned就是无符号的,注意char有3众,一个是char一个是signed char 一个是unsigned char,实际上就只有2种一个是signed char 和unsigned char char根据编译器决定他是signed还是unsigned
c++标准并没有规定带符号类型该符合表示 ,但是约定了在表示范围的正负值应该平衡,因此8比特的signed char理论上应该表示-127到127区间内的值,大多数的现代计算机将其实际的值表示为-128到127
char到底是用ascii还是别的了?取决于计算机支持的编码方式,
在这里插入图片描述

unsigned u = 10;
int i = -42;
std::cout << i+i <<  std::endl;
std::cout << u+i << std::endl;  
//结果为	-84 和4294967264  
//首先unsigned u是简写,其全程为unsigned int u,unsigned int 类型的值和 int 类型的值相加,结果是 unsigned int 类型,
这是从c语言继承过来的,int默认是有符号的。所以我们首先将int i这个i转换成无符号整数,负号(有符号类型)转换成无符号类型,
首先,计算机底层都是以补码的方式存储数据这样好做加法减法运算,-42的补码是4294967254(-42的反码加1,-42的源码最高位为
1因为他是负数,一共32位因为这里int位32位)+10,就是-42的补码正好是一个正数,所以直接就加上10,但是如果加上的值超过了32位的限制
比如加上43,等于4294967297,32位最大为4294967296,超出了一位,那么就回到1

c++字符串也是由数组构成的数组构成的,而且最后一位为\0不管是c风格创建字符串还是c++ string类型创建字符串他的最后一位一定是\0(c++ 11新标准规定string最后一个一定是\0但是c++11之前没有规定所以支持c++11之前的的编译器有的有有的没有)
c++转义序列
在这里插入图片描述
常量表示
在这里插入图片描述

2.2 变量

变量的申明就是使得这个变量的名字为程序所知,
定义就是负责创建与名字关联的实体
声明规定了变量的名字和类型
定义在声明之上又为他创建空间,有可能分配了初值
如果要定义一个变量而不是声明他在前面加上extern关键字

extern int i; //声明int变量i,如果extern显示的给他赋予一个值那么就给他开辟了空间并且赋予初值,了这叫做定义。  
extern int i {1}; //定义  
int i; //定义变量i

如果再多文件系统中使用同一个变量,必须定义和声明分离,此时变量定义只能出现在一个文件中,而其他用到改变量的文件必须对其进行声明,绝对不能重复定义

标识符:
c++的标识符用字母,下斜线,数字组成,其中必须以字母和大小写开头,对大小写字符敏感 ,关键字,操作符代替名也不能当成标识符。
在这里插入图片描述

在这里插入图片描述
标识符的作用域,c++的作用域大部分以大括号分割

#include <iostream>
int main ()
{
 	int sum; 
 	for (int val=1;val<=10;++val)
 	{
 		sum += val;
 	}
 	std::cout << "sum 	of 1 to 10 inclusive is "<< sum <<std::endl;  
 	return 0 ;
 
}

此程序定义了main,sum,val,使用了命名空间名字std,该空间提供了2个名字cout和cin
名字main再所有的大括号之外,他和大多数定义在函数体之外的名字一样拥有全局作用,,一旦声明之后全局作用域内的名字可以在整个程序的范围内都可以使用,名字sum定义与main函数所限制的作用域之中,从声明sum到main函数结束为止都可以访问它,但是出了main他就不能访问了,语句val定义在for中(不要在c语言的for定义中定义变量,这样是未定义代码),只可以在for中访问val但是出了for在main函数其他的区域就不能访问了,
for循环中内嵌的或者说被包含的作用域叫做内嵌作用域
包含着别的作用域的叫做外作用域,
如果在外作用域创建了一个变量,那么这个外作用于中所有的内嵌作用域都可以使用这个变量 ,而且内嵌作用域也可以起和外作用于一样名字的变量而且不会随着内嵌变量的值改变而改变,他们存储的位子有可能不一样

#include <iostream>
int reused = 42;
int main 
{
	int unique = 0; 
	std::cout << reused << " " << unique << std::endl;  
	int reused = 0;
	std::cout << reused << " " << unique << std::endl;  
	std::cout << ::reused << " " << unique << std::endl;
	return 0; 
}  
//第一次cout输出 42 0 
//第二次输出 0 0 因为在内嵌作用域的又定义了一个reused变量和全局作用的一模一样,然后里面的reused覆盖了外面的但是并没有改变外面reused的值,也就是说在main里面再使用reused就是0了。  
//第三次输出42 0 因为这次输出的是::reused,这个意思是显示的访问全局标量,进一步的证明我们的这个全局变量reused没有因为内嵌作用域中创建的reused变量的值改变而改变。

2.3 复合类型

c++好几种符合类型我们这里说2种引用和指针
复合类型的定义就是基于其他类型定义的类型,比如数组,指针,引用等等

2.3.1 引用 (reference)

引用就是为对象起了另外一个名字
int ival = 1024;
int &refVal = ival ; //refVal指向ival(也就是说refVal是ival的另外一个名字)
int &refVal; //报错:引用必须初始化
绑定并不是讲原来的值赋给引用,而是等ival初始化完成后,讲ival和refVal绑定在一起(时对象相互绑定),因为无法令引用重新绑定到另外一个对象,所以引用必须初始化。
refVal = 2; //将refVal赋一个值相当于给ival赋值,因为refVal为ival的引用
int i = refVal; //正确,获取引用的值其实就是获取绑定对象的值
int &refVal2 = refVal; //这样相当于refVal2是ival的引用,也就是这个变量最终还是绑定到ival上 在c++种不能定义引用的引用 因为引用并不是一个对象,而我们前面的int &refVal2=refVal不是引用的引用,int &(&refVal)这个才是引用的引用。
在这里插入图片描述

2.3.2指针

指针就是指向一个地址,指针变量就是一个地址
我们获得地址用地址运算符&,这个和我们的引用一样但是意思完全不一样
int *a,b; //定义int类型指针变量a,int类型变量b
b=10;
a = &b; //将b的地址赋给指针变量a
cout << *a(取出指针a指向的值)
在这里插入图片描述

在这里插入图片描述

空指针
空指针即为不指向任何对象,创建空指针
int *p1 = nullptr; //创建一个空指针c++11的方法 ,nullptr是一个预处理变量
int *p1 = 0;
int *p1 = NULL; //他在头文件cstdlib中定义,他的值就是0
在这里插入图片描述
我们的指针也可以当成条件加入if语句中
例如

int a=10;
int *p1 = nullptr;  
int *p2 = &a;  
if(p1)  
...
else if(p2)  
...  
//当指针为空的时候条件为假也就是0,当指针不为空的时候条件为真也就是1  
//仍和非0的指针式都是true  

int *p,p1; //*是修饰p的,所以这是定义一个int类型的指针,和一个int类型的变量p1
int* p,p1;//基本数据类型是int而不是int *,所以意思是定义了一个int类型指针p,一个int类型变量p1和上面的一样

指向指针的引用
因为引用不是一个对象,所以不能定义指向引用的指针。但是指针是对象,我们可以定义引用指针的引用。

int i = 42;  
int *p;  //p是一个int类型指针  
int *&r = p; //r是一个对int类型指针的引用
r = &i; //因为r是对p指针的引用,所以对r赋值就是对指针p赋值,也就是令p指向i
*r = 0;//解引用r得到i,也就是p指向的对象,,然后将i的值赋给0  
//解引用就是得到引用对象(指针)指向的变量的值。  
//像*&r这样的句子怎么去识别他了?有一个诀窍,离变量名字最近的符号对变量类型有最直接的影响因此r是一个引用 int *代表r引用的是一个int指针

2.4 const限定符

const也就是read-only指定变量是只读的但是当const碰上了指针和引用就变得复杂了

const int bufsize = 512; //输入缓冲区的大小,这样bufsize就变成了一个常量。仍和试图向bufsize写入数据的操作都会报错。  

const对象一旦创建后起对象就不能被更改,,所以const对象必须要初始化(和引用一样,当我们创建引用的时候必须要初始化,因为引用创建后就不能更改绑定)

const int i  = get_size(); //正确  
const int j = 42; //正确  
const  int k;  //错误必须要初始化

默认情况下const仅仅在文件内有效

*********file1.cc**************
extern const int bufsize = fcn();  //在file1.cc定义了一个extern const的变量,当我们在其他的文件种声明这个变量就可用这个变量  
*********file1.cc***************


**********file2.h*****************
extern const int bufsize; //声明这个bufsize变量后我们就可以使用file1.cc里面的这个变量了并且bufsize是read-only的
**********file2.h*****************

2.4.1 const与引用

const int ci = 1024; //定义一个常量ci  
const int &i = ci; //引用了一个常量,并且这个引用也是常量  
i = 42; //错误,试图通过引用去修改常量ci的值  
int &i1 = ci; //错误如果引用的对象是一个常量,那么这个引用也必须是常量,不然就可以通过这个引用&i1去访问修改ci了 ,如果ci不是read-only的也可以被i1引用,因为这是单方面的

我们看一下的代码

double dval = 3.14;   
const int &ri = dval ;  
上述的代码将一个read-only int类型的引用,去引用一个double类型的变量,这没有错,我们看看编译器对这个特殊情况做了什么
const int temp = dval;  //由双精度浮点数临时生成一个整形的常量  
const int &ri 	= temp; //让ri绑定这个临时变量

在这里插入图片描述
如果当ri不是一个常量的时候我们可以直接通过ri给dval赋值但是,我们编译器在他们中间加了一个temp变量,这时ri引用的是temp而非dval,这样不能赋值那么引用他干什么了?所以ri不能是非read-only的引用,c++也视为他为违法

2.4.2 指针和const

const double pi = 3.14; //pi是一个常量,不能改变其值  
double *ptr = &pi; //错误pi是一个常量,如果这样指的话我们可以通过ptr指针直接改变pi的值  
const double *cptr = &pi;//正确  
*cptr = 42; //错误不能给cptr赋值因为他是const的  

我们都知道指针的类型和指针所指向的对象类型必须一致,但是有一个例外

double dval = 3.14;  
const double *ptr5 = &dval;  //正确,但是不能通过ptr5去更改dval的值,和引用一样  

指针常量和常量指针
首先
指针常量

对象是一个常量,也就是指针本身是常量)

int * const   i   
//我们应该从左往右读,就是指针常量   
//如果要看他的意思的话就从右往左看,首先他是一个const变量,然后他是一个指针

常量指针

首先对象是一个指针,他指向的对象是一个常量

const int * i  
// 从左往右读,常量指针  
//如果我们要看他的意思的话就从右往左看,首先他是一个指针,然后是指向int类型

2.4.3 顶层const和底层const

顶层const指,指针本身就是一个常量,而底层const代表他所指向的对象是一个常量

int i = 0 ; 
int *const p1 = &i; //p1本身就是一个const他的指不能改变,所以他是顶层const  
const int ci = 42; //ci本身的值不能改变所以他是一个顶层const  
const int *p2 = &ci;  //首先p2是一个指针,他指向的值是一个const,所以他是底层const  
const int *const p3 = p2; //p3是一个const的类型 p2也是一个const的类型  
const int &r = ci; //用于申明引用的都是底层const 

//拷贝不会改变其中的值,所以我们的顶层const可以做拷贝给别人的操作,但是我们底层const就不能随便拷贝给别人,因为我们的底层const指向的对象是一个const的,所以他只能拷贝给底层const   

int * i = p3; //p3是底层const但是i不是  
const int *i = p3; //正确i是一个底层const  
//我们这里有一个例外就是p3,它既是底层const也是顶层const,我们把它当成底层const不能随便的传递给别人

2.4.4 constexpr和常量表达式

常量表达式(const expression)是指他的值不会改变,在编译过程就可以得到计算结果的表达式,

const int max_files = 20;  //max_files是一个常量表达式因为等号右边是一个常量,等号左边的对象也是一个常量  
const int limit = max_files; //limit是一个常量表达式,原因同上  
int staff_size = 27; //他不是常量表达式虽然27是常量但是staff_size不是  
const int sz = get_size(); //sz不是常量表达式因为sz为常量,但是get_size()他的值我们只有在运行时才能获取,我们要求的是编译过程中就要活得结果

constexpr
在一个系统中我们很难判断一个初始值到底是不是常量表达式
c++ 11标准中允许将一个变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量 例如

constexpr int mf = 20 ; 
constexpr int limit = mf +1; 

指针和const
必须明确一点在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指向得值无关

const int *p = nullptr; //p是一个指向整型常量的指针  
constexpr int *q = nullptr; //q是一个常量  

2.5类型别名

有的类型非常的多名字非常的难记,比如说我们创建了一个结构struct结构里面有那么多的东西我们用别名就可以代替

typedef struct 
{
	int i;
	double b;
}struct_t;  
//我们为结构struct类创建了一个别名struct_t以后我们定义这种类型就直接写 struct_t i;即可
typedef double wage; //double的别名是wage以后定义double类型我们可以写成   wage  i;即可  

c++11标准规定了一种新的标准using 别名声明
using SI = int ;
SI i; //等同于int i;
如果某个类型别名指代的是一个符合类型,它会产生意想不到的结果

typedef  char * psting;  //psting是char *的别名
const psting cstr = 0 ; //cstr是一个指针指向一个char类型的常量   
const psting *ps;	//ps是一个指针他的对象是一个指向char类型常量的指针   
//他不能理解成const char * * ps   我们要把psting看成一个整体
  

auto类型说明符
auto可以自己判断符号比如

double i = 18356.16386;  
double i1 = 86149.12862;  
auto item = i+i1;  
//auto会自动的判断i+i1后该数字属于什么类型的变量,然后给item定义成什么类型的变量,正如此例item的类型为double,并且auto必须要有初始值  
auto i = 0 , * p = &i;  //正确i=0位整型所以p位整型指针  
auto sz = 0,pi = 3.14; //错误,sz为整型,但是pi为浮点型,因为一条申明语句只能有一个基本的数据类型,所以语句中的数据类型必须一样
int  i = 0 ,&r = i;  
auto a = r ; //虽然r是一个引用auto也会根据引用所绑定的类型去确定a的类型,所以这里a是int类型
//如果我们的赋的值是一个指针了? 还是看对象  
const int ci = i,&cr = ci; //cr是一个引用  
auto b = ci;  //b是一个整型变量,因为ci是一个整型变量虽然他是顶层const  
auto c = cr; //c是一个整数,因为cr是一个引用引用的是一个整数  
auto d = &i; //d是一个整数指针,因为i是一个整数  
auto e = &ci; //e是一个指向常量的int类型指针也是一个底层const
//如果希望类型是一个顶层const则如下  
const auto f = ci;  
auto &g = ci; //g是一个整型常量引用,绑定到ci  
auto &h = 45; //错误不能为常量绑定上一个非常量引用  
const auto &j = 42; //正确

2.5.1 decltype类型指示符

有的时候我们希望从一个表达式的类型判断出要定义的变量类型,但是不想用该表达式的值初始化变量c++11 引进了decltype,他的作业就是返回操作数的数据类型,程序分析表达式并且得到他的类型但是并不会去实际计算他

decltype (f()) sum =x ;  //程序不会调用f(),而是使用当调用发生的时候f函数的返回值类型作为sum的类型  
const int ci = 0 ,&cj = ci;  
decltype(ci) x = 0; //x的类型是const int,他和auto有一些不同  
decltype(cj) y = x;  //y的类型是const int &,y绑定到变量x  
decltype(cj) z; //错误z是一个const int 类型的引用必须要初始化(引用就要初始化)  

2.6.1 自定义数据结构

我们现在所学的还不能写出完整的类,我们可以先写一个数据结构他和c语言的定义数据结构有点像,

struct abc 
{
	int a;
	int b;
	int c;
};

struct abc var1; //c style
abc var1; //c++ style 
struct sales_data
{
	std::string  bookno;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
//我们详解一下这段代码,从struct开始紧紧跟这的是类名(sales_data)和类体({}内的),类体通过花括号包围成
一个新的作用域,类内部的名字必须唯一,但是可以与类外部定义的名字重复,类体结束后必须要跟一个;因为
类体后面可以直接跟变量名,我们也可以这样写  

struct sales_data {/***/} var1,var2;
#也可以这样写  
struct sales_data {/***/} ;
sales_data var1,var2;

类体定义了我们类的数据成员,数据成员定义了类的对象的具体内容我们上面定义的有3个数据成员一个是bookno,一个是units_sold,一个是revenue他们的类型分别是std::string,unsigned int,double,在c语言中我们定义类并没有为他的成员设置初始值,但是c++11开始我们定义类可以为他成员设置初始值,所以当我们定义sales_data类型对象的时,bookno,units_sold,revenue都会被初始化成默认值。我们到后面会学习到一个新的关键字class来定义自己的数据结构

6.5.2 使用sales_data类

首先我们添加2个sales_data类型的对象 ,假设sales_data定义在sales_data.h内,我们这样使用

#include <iostream>
#include <string>
#include "sales_data.h"
int main()
{
	sales_data data1,data2;	//定义2个sales_data类型
}

因为我们的sales_data类型中有一个成员需要用到string头文件定义string类,所以我们加上它

string类就是字符串的序列他的操作有>>,<<和==等,功能分别是读入字符串,输出字符串,和比较字符串

我们将信息输入到data1中

std::cin >> data1.bookno >> data1.units_sold >> price;  /*当我们输入一个字符串中间是空格或者是tab或者是换
行和第二个隔开再以空格或者tab或者是换行隔开,这样输入到3个变量中*/  
//我们再计算销售输入  
data1.revenue = data1.units_sold*price; //.操作符就是访问成员类型的

6.5.3 编写自己的头文件

我们可以在函数体内部定义类,但是这样会让代码很长,而且会受一些限制,灵活性也不高,所以我们一般将类定义在外部,或者别的文件
为了确保各个文件中的类一致(有可能要在多个文件中用一个类),类通常被定义在头文件中,而且类所在的头文件的名字应该与类的名字一样,例如我们的sales_data类定义在文件sales_data.h中我们的头文件有可能用到其他头文件的内容比如我们的sales_data用到了string头文件的内容 ,在我们使用sakes_data成员bookno的时候不管是存入还是别的会再调用一次
我们确保头文件多次包含仍能够安全工作的常用技术是预处理器,预处理器是在编译之前执行的一段程序,之前我们已经用到一项预处理器功能 #include,当预处理器看到#include后会用指定的头文件的内容代替#include
c++还会用到的一项预处理器功能是头文件保护符,头文件保护符依赖于预处理变量,预处理变量有2中状态:以定义和未定义,#define的指令是吧一个名字设定为预处理变量,另外2个指令分别检查某个指定的预处理变量是否已经定义,#ifdef当且仅当变量已经定义为真,#ifndef当且仅当变量定义为真。一旦检查为真,则执行后续的操作直到遇到 #endif为止,这样可以有效的避免重复包含的发生。

#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct sales_data 
{
	std::string bookno;
	unsigned units_sold =0;
	double revenue = 0.0;
};
#endif
/*我们来解释一下,当我们预处理器执行第一项的时候发现SALES_DATA_H这个变量没有被定义就执行下面的代码直到#endif
我们再看第二段代码#define SALES_DATA_H这一段代码就直接定义了SALES_DATA,代表下一次包含这个头文件的时候不生效。。。
然后我们就执行下面代码,注意我们#include这个头文件的时候要写这个头文件的绝对路径(linux下)
比如
#include /root/test/XXX.h*/   	#最好的方法

当文件中有对变量的定义时,多次包含该文件这个变量就会被多次定义,这就会报错。
如下: 当文件中有一个对变量的定义
在源代码上是这样的
#include <sales_data.h>
#include<sales_data.h>
//这样我们包含了2次与编译器就会代替2次sales_data.h头文件中的源码
int value = 10;
当多次包含该文件时,就变成了
int value = 10;
int value = 10;
··· ···
int value = 10;
这样就会出错。

3.0字符串向量属组

前言,c++出了内置的类型外还定义了非常丰富的抽象数据类型库,比如string,vector,前者string是可支持边长字符串,后者则可以表示变长集合,他们都是对于内置数组的抽象

3.1 using

我们之前用到的库函数都是属于命名空间std,并且我们显示的表示了这一点std::cin(::为域操作符),他代表从::左边名字所表示的区域内找到右侧的那个名字,所以std::cin为使用std命名空间中的cin,如果我们使用可using声明则不用再加上前缀,格式如下

using NAMESPACE::NAME;

一旦声明了上述的语句我们就可以直接访问命名空间中的名字了

#inlude <iostream>
using std::cin;
int main()
{
	int i ; 
	cin >> i; //正确因为我们前面定义了using std::cin
	cou << i;  	//错误我们只声明了std::cin并没有声明cout
	std::cot << i ;	//正确,我们没有声明cout只能显示的写出
	return 0 ;
}

按照这种规定每个using声明引入一个命名空间的成员所以我们也这样写

#include <iostream>  
using std::cin; 
using std::cout;using std::endl;  
int main()
{
	cout <<  "enter two numbers" << endl; 
	int v1,v2 ; 
	cin >> v1 >> v2;
	cout << "the 	sum of " << v1 << " and "<< v2 << "is" << v1+v2 << endl;
	return 0;
}

头文件不应该包含using声明,如果我们的头文件使用了using声明别的文件调用了这个头文件,调用者就会包含这些声明,在代码中不经意包含一些名字可能会产生意想不到的效果

3.2 标准库类型string

标准库类型string表示可变长的字符序列,使用string类型必须首先包含string。作为标准库的一部分,string定义在命名空间std中,我们后面写的代码默认加上了 #include <string> using std::string

3.2.1 定义和初始化string对象

如何初始化类的对象是由类本身决定的,一个类可以由很多种初始化的方式,一下列出了string对象最常用的一些初始化方式

string s1; 	//默认初始化,s1是一个空字符串
string s2=s1;  	//s2为s1的副本
string s3="honky";	//s3为该字符串的副本
string s4 {10,'c'};	//s4内容为cccccccccc,也可以解读成s4由10个字符c组成 
string s5("valus");	//s5为“valus”的副本等价于,stting s5 = "valus";  
string s6(s5);	//s6是s5的副本

从上面可以看出如果使用=号初始化一个变量,实际上是执行的是拷贝初始化,如果不适用=号初始化则执行的是直接初始化

3.2.2 string对象的操作

一个类除了要规定初始化其对象的方式外,还要定义其对象上所能执行的操作

getline(is,s) //从is里面读取一行返回给s
s.empty() //如果string类型s为空就返回true如果为假false
s.size() //返回string类型s中字符的个数
s[n] //返回string类型s中第n个字符串,从0开始计数
s1+s2 //返回连接string s1和string s2的结果
s1 == s2 //判断s1和s2中所含的字符是否完全一样 如果完全一样就返回真区分大小写
s1 != s2 //判断string s1和string s2是否相等,如果不相等就返回真,区分大小写
<,>,<=, >= //这种比较符在后面会详细提起

我们也可以用io操作符读写string对象  
int main()
{
	std::string s;	//空白字符
	std::cin >>s;  //从缓冲区中读入对象s,直到遇到空白为止(遇到空格,回车当然回车就直接返回了,tab就停止读,当然如果第一个字符为空或者回车就跳过直到遇到第一个字符才开始读入,并且读完后不会清除后面的缓冲区包括\n) 
}
int main()
{
	string word;
	while (cin >> word)	//不断地从缓冲区读入到word直到遇到文件结束符
		cout << word << endl;	//逐个输出
	return 0 ;
}

getline
我们希望读取的时候保存空白字符(>>不能保存遇到空格就退出这次读取),我们可以使用getline函数代替原来的>>运算符,
getline的参数是一个输入流和一个string对象,,函数从给定的输入流中读取数据直到遇到\n为止,而且getline并不会读取\n,最后endl会返回回车并且刷新缓冲区

size函数返回string的对象长度

	string line;  
	while (getline(cin,line))
	 	if (line.size() > 80)	//打印出超出80个字符串的行其中输入\n是2个字符空格为一个字符
	 		cout << line << endl;

string类中定义了几种比较字符串的运算符== != <= < > => 其中比较大小的关系运算符对比规则如下

1,如果2个string类型的变量长短不一,而且较小变量和较长变量一 一对应完全一样,则可以说字符少的变量小于字符较长的变量例如"hello"和"hello,"这两个变量前五个完全一样但是后面的变量比前面的变量多了一个,所以后面的大
2,如果2个string类型变量长短不一,而且他们下标对应的内容不一样比如,"helloworld"和"honky"从第二个字符开始就不一样了所以不能用第一个规则用第二个规则,他们从第二个字符开始不一样的并且o在字典上比e大所以"honky"比"helloworld"大,这不是单单的比较长度了

对string对象赋值

string st1(10,'c'),st2;	#st1d的内容是cccccccccc   
st1=st2;	#将st2的副本赋值给st1,此时st1和st2都是空字符串

2个string对象相加
2个string对象相加其实就是将+号左侧的运算对象和+号右侧的运算对象相加

string s1(8,'a'),s2="hello",s3;	#定义一个s1字符串aaaaaaaa,和s2字符串hello,和一个空的s3字符串
s3 = s1+s2;  #s3等于aaaaaaaahello,他将s1和s2拼接在一起   
s1 += s2;	#s1等于原s1+s2也就是 aaaaaaaahello  

字面值和string相加
字符字面值和字符串字面值和string类相加标准库允许他们转换成string类型

string s1 = "hello" ,s2 = "world";	  
string s3 = s1+", "+s2+'\n';	//此时", "和‘\n’被转换成了string   
//注意把string和字符字面量和字符串字面了一起拼接的时候必须要用+ ,切记字面值不能直接相加   
string s3 = "hello"+'\n'+s1;	//错误字面值不能直接拼接
3.2.3 处理string对象中的字符

我们经常要单独处理string中的字符,所以我们有一个头文件cctype来处理,他有很多标准库函数专门用来处理字符
在这里插入图片描述
在这里插入图片描述
所以从这个看出来我们的cctype在c语言中是ctype.h在c++中是cctype

c++11中定义了一种新额for循环,这个for循环是基于范围的for循环 ,它可以遍历给定序列中的每个元素并对序列中的每个值来执行某种操作其语法是

for (declaration: expression)  
	statement 
//expression是一个对象他表示一个序列,declaration是一个变量这个变量用于访问序列的基础元素,每一次迭代declaration都会被初始化成expression部分的下一个元素值,我们拿string来举一个例子  

string str1("helloworld");  
for (auto c:str1);  
/*在此例子中c的类型有编译器决定,也就是给c复制的值的类型,每次迭代都是char,所以c是一个char,举一稍微复杂点的例子*/  

string s("hello world!!!");  
decltype(s.size()) punct_cnt = 0;  	//s.size计算s的字符串长度并且返回unsigned int类型复制给变量punct_cnt  
for (auto c: s)	//将s的元素从第一个开始复制给c然后执行下面的语句完成第一次迭代再到第二个元素赋给c,
	if (ispunct(c))	//如果c是标点符号就返回true不然就返回false  
		++punct_cnt ;	//此变量自身+1  
cout << punct_cnt << "个标点符号" << s <<endl;  
/*上面这个例子中他的意思是先定义一个字符串str1,然后再定义一个变量punct_cnt这个计数器,他的类型是s.size()返回值的类型也就是
int,然后将变量c放到字符串s中每个元素每个元素的迭代,c的类型是s每个元素的类型也就是赋值给c的类型,然后判断他是否为标点
符号是的化计数器punct_cnt+1*/ 


 

我们如果要访问所有的元素用for是一个好的办法但是我们只访问其中某个元素或者到某个元素为止,使用下标和迭代器非常的好用,例如我们下面的例子中吧第一个词改为大写

string s("some string");   
for( decltype( s.size() ) index = 0; index != s.size() && !isspace(s[index]); ++index )  
	s[index]  = toupper(s[index]);  
	/*首先我们定义一个string类型字符串,然后其中for循环是传统c风格的for循环,其意思是,使用decltype获取s.size()返回数的类
	型,然后赋给index变量,此时index变量就是s.size返回值的类型,然后如果s.size()的返回值不等于index,s.size返回的是字符
	串的长度,其为11,&&符号为左右都为真才返回真,如果有一个为假就返回假,然后左边的确不等于index所以返回真,当index>
	字符串长度的时候才返回假,&&右边判断s[index]是否为空为空就返回真,但是他有一个!说明去翻为空就返回假,最后index自身
	+1,其中句子的意思很简单,toupper函数的意思是吧字符串改成大写返回*/

在c++中当&&左侧为真的时候才会检查右侧运算对象结果的值,这点我们要注意

3.3 标准库类型vector

vector也是一个类型他表示对象的集合,其中所有对象的类型都相同,每一个对象都有一个对应的索引
vector模板提供的是一个动态数组,其数组每个元素的类型是我们初始化vector模板时后面尖括号内指定的类型比如我们初始化一个vector模板
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
但是有一个例外我们的初始化vector时如果使用了花括号的形式但是提供的值又不能用来列表初始化比如我们的每个元素类型是string而我们给他提供了int,那此时我们就要想他是否是构造例如下图第三行和第四行,当我们的编译器看到是大括号的时候发现里面的值是一个int而我们定义vector模板初始化的时候用string类型,直接初始化是不成功的,此时编译器会尝试构造,从此看出c++非常灵活
在这里插入图片描述

3.3.2向vector对象添加元素

在这里插入图片描述

在这里插入图片描述
vector其他操作

在这里插入图片描述

#include <iostream>
#include <vector>
using namespace std;
int main()
{
        vector<int> v1 {1,2,3,4,5,6,7,8,9};
        for(auto &i:v1)
        {
                i *= i ;
        }
        for(auto &i:v1)
        {
                cout << i << " " << endl;
        }
        return 0 ;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.4迭代器

在这里插入图片描述

3.4.1使用迭代器

可以使用迭代器的类型拥有返回迭代器的成员函数,此类型都有begin和end成员,begin代表第一个元素的迭代器,end代表最后一个元素的下一个位置 ,也就是说他返回一个容器内不存在的尾后元素,它仅是一个标记,表示我们处理完了容器中所有的元素,end成员返回的统称为尾后迭代器,如果容器为空则begin和end返回同一个迭代器
下图列举了迭代器的一些运算
在这里插入图片描述

和指针类似,迭代器也可以去通过解引用来获取他所指示的元素,执行解引用的迭代器必须合法并且确实的指示者某个元素,试图解引用一个非法的迭代器或者尾后迭代器都是未定义行为

“‘解引用’,我倒觉得可以从另一个角度理解,”*“的作用是引用指针指向的变量值,引用其实就是引用该变量的地址,“解"就是把该地址对应的东西解开,解出来,就像打开一个包裹一样,那就是该变量的值了,所以称为"解引用”。也就是说,解引用是返回内存地址中保存的值。”
(转)

我们写一个小程序看看 ,利用迭代器将string对象的所有字母改成大写模式

#include <iostream>
#include <string>
using namespace std;
int main()
{
        string s("some string");
        auto it = s.begin();	//将首迭代器赋值给it
        while(it != s.end())	//如果it(现在第一次循环是首迭代器)不等于尾迭代器,也就是说中间有值,那么就执行下面的操作使用toupper成员函数将it的解引用赋给it的解引用也就是把他的大写赋值给他
        {
                *it = toupper(*it);
                ++it;	//向后迭代一个
        }
        cout << s << endl;
        return 0 ;
}

迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->,*,++ --等封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作;

我们也可以这样写

for(auto it = s.begin() ; it != s.end()  ; ++it )
	*it = topper(*it);

迭代器类型
在这里插入图片描述

在这里插入图片描述

begin和end运算符
begin和end返回的具体类型是由对象是否常量决定的,如果是常量则返回const_iterator,如果不是常量则返回iterator
在这里插入图片描述
在这里插入图片描述

c++11还专门的引入了cbegin和cend专门用来得到const_iterator类型,他们不管这个迭代器代表的对象是否是常量都返回const_iterator

auto it3 = v.cbegin();	//it3是vector<int>::const_iterator

我们解引用迭代器后是一个类,我们有可能还要再进一步的访问他的成员,比如一个vector<string> v1; auto a = v1.begin();v1是一个vector<string>类型,我们定义他的首迭代器然后赋值给a,然后我们解引用(*a)后得到的是容器内第一个元素也就是v1中第一个string类型,我们还可以再进一步的访问这个string的成员,

(*a).empty();	//解引用后得到第一个元素,第一个元素是字符串,判断字符串是否为空
*a.empty();	//错误,这个表达式先从a这个迭代器中寻找empty成员,这个是错误的,所以我们要先加上(*a)

c++中引用了->把解引用和成员访问绑定在一起

(*a).empty();  
a->empty; 
#上述两个式子是一模一样的 

注意!我们前面提到千万不要用for循环去向vector对象中添加元素,还有一个禁忌是不要在vector添加元素的时候比如push_back等操作时都会使vector迭代器失效

3.4.2迭代器运算

我们的标准容器都支持迭代器,也支持++,-- ==,!=运算,我们的vector容器和string的迭代器支持更多的运算,它可以一次跨越多个元素等操作

在这里插入图片描述

我们举个例子

auto mid = vi.begin() + vi.size()/2;  
#如果vi有20个元素则vi.size()等于20然后/2得到10,然后首迭代器加10,代表vi这个vector向后移动10位也就是vi[10];最后赋值给mid

当我们2个迭代器相减得到的是一个这2个迭代器的距离,这个距离的类型名是difference_type的带符号整数类型,string和vector都定义了difference_type类型,因为这个距离可正可负,

使用迭代器运算
使用迭代器的一个经典运算就是2分查找
在这里插入图片描述

所以我们用迭代器写一个二分查找

#include <iostream>
#include <vector>
using namespace std;
int main()
{
        vector<int> v1{1,5,8,10,13,16,18,20,22};
        auto bg = v1.begin();
        auto ed = v1.end();
        auto mid = bg+(ed-bg)/2;
        signed int i ;
        cin >> i ;
        while( *mid != i)
        {
                if(i < *mid )
                {
                        ed=mid;
                        mid=bg+(ed-bg)/2;

                }
                else
                {
                        bg=mid+1;
                        mid=bg+(ed-bg)/2;
                }
        }
        cout << *mid << endl;
        return 0 ;
}

3.5数组

数组和vector模板初始化出的对象非常像,区别就是vector是动态数组,
数组是一种符合类型数组申明如a[d],其中a是数组的名字,d是数组的维度,也就是元素个数,因此d必须大于0,数组中的元素的个数也属于数组类型的一部分,编译的时候维度应该已知的,也就是是说维度必须是一个常量表达式
在此我们有以下几点要注意

1,在我们创建数组的时候,维度必须是一个常量我们可以用const,constexpr,#DEFINE来表示常量,
2,我们不能在定义数组的时候使用auto去推断他的类型

3.5.1初始化数组

我们可以用列表初始化,在我们列表初始化的时候可以不指定维度,我们的编译器会从我们的列表中推算出维度,在我们的维度大于初始化的列表的时候,其余没有对应上维度的数组一律使用默认初始化,当让,维度小于初始化列表的元素格式就会报错

const unsigend sz = 3;  
int ial[] ={0,1,3};	//合法,编译器通过列表自动的推算出维度   
int a3[5]  = {0,1,2};//最终a3等于{0,1,2,0,0}  
string a4[3] = {"hey","bye bye"};	//a4等于{“hey”,“bye bye”,“”}
int a5[2] = {0,1,2};	//报错,初始化的值太多

在这里插入图片描述

理解复杂的指针

int *a[10];	//a是一个含有10个整形指针的数组   
int &b[10];	//错误,不存在引用的数组   
int (*c)[10] =&arry;	//c指向一个含有10个整数的数组  
int (&d)[10]  = arr;	//d是引用一个含有10个整数的数组   
int *(&e)[10] = ptrs;	//e是一个数组的引用改数组有10个指针

首先我们开始讲过看这种非常复杂的修饰定义默认从右往左看,但是遇到括号就先看括号再从右到左所以
第一个首先他是一个有10个元素的数组,数组中的对象是指针,指针的类型是int
第二个错误,我们来分析一下,首先他是一个有10个元素的数组,他中间的类型是引用,但是我们引用需要初始化,这里没有初始化错误
第三个加了一个括号,所以我们先看括号,他是一个指针,再看右边,他是一个指向10个元素的数组的指针,再观察左边,数组的元素是int
第四个:首先看括号他是一个引用,引用的对象是一个带有10个元素的数组,再看最左边的int表示数组中的元素是int类型,并且已经被初始化了,所以他是正确
第五个:首先看括号他是一个引用,再看右边他引用的对象是一个数组有10个元素,再看左边改数组的元素为10个int类型指针

为什么引用要被初始化? 因为引用的第一步是绑定对象,所以定义引用必须要有初始化对象

3.5.2访问数组元素

和访问vector初始化对象和string一样可以用for( : ),下标运算来访问,数组除了大小固定外,其他的用法与vector类似

3.5.3指针和数组

在我们使用数组的时候编译器一般将他转换成指针,而且我们的数组还有一个特性就是在只使用数组名的时候编译器自动将他转换成数组首元素的指针 (和C语言一样)
我们在前面说不能用auto就是这个原因,例子如下

int ia[] = {0,1,2,3,4,5,6,7,8,9};	//定义一个数组  
auto ia2(ia);	//ia2是一个整形指针指向ia的的一个元素而且ia就是ia的第一个元素的指针  
ia2 = 42;	//错误!ia2是一个指针 
但是我们使用decltype就不一样了decltype是取这个字面的意思,  
decltype(ia) ia3 = {9,8,7,6,5,4,3,2,1,0};	//真确ia3的数据类型为一个含有10个元素的数组

auto和decltype的区别:
auto a = i1+i2;
我们的auto在编译器阶段会去运行计算右值的表达式,将值的类型赋给a
decltype(i1+i2) a;
我们的decltype在编译阶段不回去运行i1+i2而是推导他
auto会忽略掉顶层const但是会保存下底层const比如

const int a = i; 
auto b = a;	//b是int因为a是顶层const,auto自动过滤顶层const所以是int 
auto c = &a;	//a是顶层const但是&a是a的地址也就是底层const所以c为int *也就是指向整数常量的指针  

我们的decltype处理顶层const和底层const和auto不一样,如果decltype里面是一个表达式,则返回改变量的类型,不管这个表达式是顶层const还是引用

const int ci =0,&cj=ci;
decltype(ci) x;	//因为ci是顶层const则x为const int  
decltype(cj) y = x;	//因为cj是一个引用,引用到整形常量ci,我们的y也是一个const int &

指针也是迭代器,
标准库函数begin和end
尽管我们可以通过计算得到数组的尾指针但是这杨极容易出错,所以我们的c++引入了更简单安全的函数begin和end我们初次一看觉得他和迭代器非常的像,但是数组不是类类型所以他们不是成员函数一下是正确使用他们的格式

int ia[] = {0,1,2,3,4,5,6,7,8,9};  
int *beg = begin(ia);	//取出ia数组的首元素的指针  
int *end = end(ia);	//取出数组ia最后一个元素的下一个元素的指针

注意他们都存在在iterator头文件中
我们可以使用end和begin函数轻松的写出一个循环并处理里面的元素 例如我们写一个遍历全部数组

int *pbeg  = begin(arr),*pend = end(arr);  
while(pbeg != pend && *pbeg >= 0)
	++pbeg;

指针可以做加减法,自加自减等等运算,我们注意当数组内2个指针相减,得到的是一种ptrdiff_t的标准库类型,和size_t类型类似不过size_t是数组中维度的类型,他们都是被定义在cstddef头文件中的机器相关的类型,因为差值可能为负值所以ptrdiff_t是一种带符号的类型。

如果我们的2个指针指向的不是相关的对象,则不能比较他们

解引用和指针运算的交互

3.5.4 c风格字符串

因为c++兼容c语言的特性所以c++可以使用c风格的字符串,但是我们要注意使用c风格的字符串容易引发程序漏洞,诸多安全问题的根本原因
c风格字符串不是一种类型,而是为了表达和使用字符串形成的一种约定俗称的写法,按此风格的写法在字符数组最后会跟着一个空字符(’\0’)
c语言提供的处理字符串头文件string.h在c++中也能用但是头文件名字需要改,变成cstring,专门用来处理c风格的字符串
在这里插入图片描述

在这里插入图片描述

次例子ca虽然是一个字符数组但是他不是以\0字符结尾,因此上述的程序是未定义的结果,strlen会从ca的内存位置一直的寻找直到遇到\0才停下来

string a ("abcdefg");	//c++ style string
string b ("abcdefgh");	//c++ style string

const c [] = {"abcdefg"};	//c style string
const d[] = {"abcdefgh"};	//c style string  
//比较他们   
if(a < b) 
{
	...
	...
	...
}

if(c < d)
{
	...
	...
	...
}

第一个if我们使用c++风格的字符串可以直接比较a地区小于b,第二个if我们相比的是指针,他们指向的并非是一个对象所以结果未定义。
相比较c风格字符串需要用strcmp函数(cstring中)就成了

if (strcmp(c,d)<0)  
{
	...
	...
	...
}
如果c=d那么返回0,如果c<d也就是前面的字符串大就返回正值,如果后面的字符串大那么就返回负值

如果我们要拼接字符串在c++中直接string1 + string2即可如果是我们的c风格字符串就有点麻烦还有点危险
如果我们c风格字符串拼接则需要2个函数strcopy()将和strcat()

strcopy(cstring,cstring1);
strcat(cstring,' ');  
strcat(cstring,string2);

strcopy将函数内后面的字符串赋值给前面的字符串我们必须要计算好后面一个字符串的长度不然就会溢出,第二个strcat为在前一个字符串后面加上后面的字符,以上的操作非常的危险如果长度计算不好就会溢出

3.5.5 与旧代码的接口

在这里插入图片描述
string提供了一个成员函数c_str

string s (“hello world”);
char *str = s;  //错误s为字符串类型无法直接赋值给str  
char *str = s.c_str;	//正确

要知道c_str返回一个指针,该指针指向一个以空字符结尾的字符
我们可以使用数组去初始化一个vector对象(注意不能用vector对象初始化数组

int int_arr[] = {0,1,2,3,4,5,6,7};  
vector<int> ivec(begin(int_arr),end(int_arr));

在这里插入图片描述
在这里插入图片描述
我们用于初始化也可以是数组的一部分
在这里插入图片描述

3.6多维数组

严格来讲c++没有多维数组,通常来说多维数组其实是数组的数组
当一个数组的元素还是数组的时候通常由2个维度定义他一个维度表示数组本身的大小,一个维度表示元素的大小

int ia[3][4];	//这个数组有3个元素,每一个元素都是数组,每个数组又有4个元素,每个元素为int  
int arr[10][20][30];    

三维数组arr同理
在这里插入图片描述
在这里插入图片描述

初始化多为数组
我们初始化多为数组有多种方法,怎么看着舒服怎么来

int ia[3][4] = 
{
	{0,1,2,3},	//第一个元素
	{4,5,6,7},	//第二个元素
	{8,9,10,11}	//第三个元素
};

上面的初始化方法我们看的最顺眼,其实里面的大括号可以不要,这里非常明显的标出了第一个元素第二个元素第三个元素和我们初始化一维数组一样不过里面的元素也是数组

int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};

上述的初始化其实和第一个一模一样不过他没有加上括号我们看着不是很舒服

没有初始化的列表我们用初始值表示

int ix[3][4] = {0,3,6,9};

上述代码只初始化了第一行也就是二维数组的第一个元素,其他的都是0

多为数组引用下标

ia [2][3] = arr[0][0][0];

上述代码将arr这个三位数组的第一个元素复制给ia的第三列的第四个(ia一共3列每一列有4个元素也就是代表最后一个元素)
我们总结得到下标所表示的是定义时所表示-1,因为下标从0开始算,而定义从1开始

int (&row) [4] = ia [1];	

上述代码首先我们看括号内代表他是一个引用再从右往左看,他指向元素个数为4的数组并且元素为int,然后引用ia的第二行那四个元素(ia [3][4])

使用范围for语句处理多维数组

int a[3][4];
int emp;
for(auto &row:2)
{
	for(auto &col:3)
	{
		emp+=a[row][col];
	}
}

指针和多维数组

int ia[3][4] = 
{
	{0,1,2,3};
	{4,5,6,7};
	{8,9,10,11}
};
int (*p)[4] = ia;
p = &ia[2];

首先第一个定义一个2维数组改数组有3个元素每个元素又是一个含有4个int类型元素的数组
第二行首先看括号为指针再从右往左看说明他指向一个含有4个元素的数组,并且元素为int,然后我们将ia也就是ia这个数组的首元素的首地址(第一行的第一个也就是0的地址)赋给指针p
第三行把ia这个二维数组的第三个元素(该元素是有4个int类型的数组也就是8的地址)的首地址给p

我们可以使用指针去打印多维数组中所有元素

for(auto p = ia;p != ia+3 ; ++p)
{
	for(auto q  = *p;q != *p+4;++q)
	{
		cout << *q << ' ';
	}  
	cout << endl;
}

上述的代码非常好理解,首先我们ia是一个二位数组(ia[3][4])第一个for循环匹配ia的字面值也就是这个数组的第一个元素(元素是数组)的第一个元素,ia+1等于匹配到第二个元素(第二个元素也是数组)的第一个元素的地址,以此类推,他的限制是p!=ia+3,也就是当p等于ia+3的时候推出
内循环是顶一个q将*p赋值给他,*p就是*ia,等于ia都代表第一个数组元素的第一个元素的地址,**p就是第一个数组元素的第一个元素的值了,第二个for循环主要是针对行的,外面的for主要针对列的

我们也可以使用标准库函数begin和end实现相同的功能而且个人觉得更简单

for(p = begin(ia); p != end(ia); p++)
{
	for( q = *p ; q != end(*p) ; q++ )
	{
		cout << *q << ' ';
	} 
	cout << endl;
}

上面使用end和begin函数变得更简单了

4:表达式

4.1表达式基础

4.1.1表达式基础概念

一元操作符比如*,&都是操作一个运算对象的
二元操作符比如*,+,==都是二元操作符
函数调用也是一种特殊的运算符
其中*不光可以代表指针也可以代表乘法,具体的意思要结合上下文

重载运算符
我们的操作符比如加减乘除用于内置类型和符合类型的运算对象所执行的操作,我们在定义类的时候可以自己定义特定的运算符另一层含义,所以成为重载运算符,io库的>>和<<运算符以及string对象vector对象和迭代器使用的运算都是重载运算符
当我们使用重载运算符的时候其运算对象的类型返回值的类型都是由该运算符定义的

左值和右值
有两个概念:变量表达式都是左值。左值可以当右值使用,并且在这种情况下用的是其内容。
所以对于“b = i”而言,是将i的左值作为右值使用,取i的变量的值,赋值给b的左值。
此外,临时对象不是const。++/- -,前缀版本返回左值,后缀版本返回右值。

4.1.2优先级与结合律

复合表达式:其指含有2个或者多个运算符的表达式,我们解复合表达式的过程就是将各个子表达式结合在一起,怎结合就是靠优先级和结合律决定
注意表达式中的括号无视上述规则,他会先得到运算
如果优先级不一样则先结合相同优先级的表达式
优先级一样的表达式则从左往右开始算比如3*4/2*6

6+3*4/2+2;	//这个式子先根据优先级计算*/再从左往右

注意括号无视优先级和结合律

4.1.3 求值顺序

虽然我们的优先级规定了运算对象的组合方式,但是并没有说明按照什么顺序执行。在大多数情况下不会明确的指定求值运算的顺序比如如下

int i = f1() * f2(); 

到底是先执行f1还是先执行f2了?再举一个例子

int i = 0;  
cout << i << " " <<++i << endl; 

在上述的例子中<<运算符并没有定义何时以及如何对运算对象求值,因此他是未定义的,有可能先求++i,有可能先求i,也有可能编译器做出别的操作,不管编译器生成什么他都是错的
在这里插入图片描述

求值顺序和优先级和结合律
首先我们有一条式子

f()+g()*h()+j()

如果按照优先级,那么就会先执行g()*h()再执行加法
如果按照结合律,那么就先执行f()加上g()*h()的结果再执行加j()
如果fghj是无关紧要的函数那么,它既不会改变同一对象的状态,也不执行io,那么函数调用的顺序不受限制,反之如果有影响那么他就是错误的表达式
在这里插入图片描述

4.2 算数运算符

在这里插入图片描述

我们只罗列了一点
我们看到的首先一元运算符的优先级要比二元运算符要高,而且二元运算符中乘除求余的优先级要比加减要高
如果优先级都一样那么就从左往右执行

算数运算符的运算对象和求值结果都是右值
当一元正号和负号运算符用作与一个指针或者算数值时,返回运算对象(被运算后)的一个副本

int i = 1024;  
int k = -i	//k为-1024,一元减法运算返回-i也就是-1024的一个副本  
bool b = true;  
bool b2 = -b;	//b2还是true

注意最后一个例子中bool值不应该参与运算,在我们运算bool的时候他被提升成为一个int类型,也就是1,他的负值为-1,再把-1的副本返回给b2,然后这个初始值不等于0(非0就是true),再转换成bool后就为true按照int打印出来就是1
在这里插入图片描述
在这里插入图片描述

c++早期的语言对于除法运算的结果如果为负则一律的向上或者向下取整,在c++11标准中他一律的向0靠其取整

4.3逻辑运算符

在这里插入图片描述

逻辑运算符和关系运算符的返回类型一律是布尔类型 ,值为0表示为假其他一律为真(和c语言非常的像)
这两类运算符的结果都是右值
对于与逻辑运算符&&来说&&左右都为真才真正的为真,否则为假(只有&&左边为真才会计算右边的表达式
对于或逻辑运算符||来说||左右只要有一个为真结果就为真 (只有||左边为假才会计算右边的表达式

关系运算
关系运算都是比较大小然后返回bool值,关系运算都满足左结合律
关系运算的结果是bool值,多个关系运算放一起有意想不到的结果

if (i<j<k)

这个语句的意思是先将i<j做比较再返回bool值再与k做比较,这样符合符合数学表达式但是不符合c++格式,我们应该这样写

if (i < j && j<k)

4.4赋值运算

赋值运算满足右结合律

int ival,jval;
ival = jval = 0;		//正确  ,因为首先0赋给jval,然后jval本身是左值,我们前面说过左值可以当右值,然后jval变成右值赋给ival

赋值运算符的优先级比较低我们通常要加上括号
我们要分清赋值运算符和比较运算符

= 赋值
== 比较

4.5递增和递减运算符

++自身递增也就是+1
–自身递减也就是-1
++和–右前置版本和后置版本,
首先前置版本

int a = 5;
int b = ++a;  
//前置版本直接返回a自身加1后的值

后置版本

int c = 9;
int d  = c--;
//后置版本会先返回c递减前的值的副本给d再进行递减

我们如果需要返回一个值的自身加1的值就不用去使用后置递增或者递减,除非我们需要保存变更之前的值
后置版本的操作步骤不前置版本麻烦,首先他要把原先的值存储下来再进行递增或者递减操作,最后返回之前存下的值,而前置版本直接递增或者递减返回即可,而我们的编译器对于指针递增递减后置版本有优化,但是对相当复杂的迭代器操作并没有。
所以最好使用前置递增递减
我们的递增递减运算符的优先级要高于一元运算符

auto pbeg = v.begin();
while (pbeg != v.end() && *pbeg >= 0 )
	cout << *pbeg++ << endl; 
//*是一元操作符他的优先级比递增递减操作符低,所以先执行pbeg++再执行*解引用
//*pbeg的意思就是先存储初始值,再将pbeg加1再返回初始值的副本做*解引用返回给cout

我们再看一个式子

while(beg != s.end() &&  !isspace(*beg))
	*beg = toupper(*beg++);	//错误未定义,toupper将字母改为大写

上述语句为何未定义???因为c/c++并没有定义先运算=号左侧还是=号右侧,而且右侧有一个*beg++的动作将会改变beg的值,如果先运算左侧,和先运算右侧结果完全不一样

4.6成员运算访问

点运算符和箭头运算符都可以用于访问成员点运算符访问对象的成员 而ptr->mem等价于(*ptr).mem 因为.运算符的优先级高于一元操作符所以要加上()

4.7条件运算符

我们可以将if-else嵌入到单个表达式中,比如

cond?expr1:expr2;
//我们先对cond进行计算如果cond的条件为真,则运算expr1表达式并且返回该表达式的运算结果,则运算expr2的表达式并返回结果 
string finalgrade = (grade < 60)?"fail":"	pass" 

在这里插入图片描述
嵌套运算符
我们的if可以嵌套,我们的条件表达式也可以嵌套例如

finalgrade = (grade > 90 )?"hight pass"
										:(grade < 60)? "fail" :"pass" ;  

//如果grade大于90就返回前一个hight pass,如果小于90那就返回后面的式子,而后面的式子也是一个条件运算符,所以再进行比较如果grade<60为真则返回fail,否则就返回pass

在这里插入图片描述
在这里插入图片描述

条件运算符的优先级非常的低我们可以在输出表达式中使用他

4.8 位运算符

程序中的所有数在计算机内存中都是以二进制的形式储存的。位运算说穿了,就是直接对整数在内存中的二进制位进行操作,比如我们有一个char类型的字符是0233(8进制),他的10进制是155,2进制是10011011

unsigned char a = 0233;   

因为再内存中是按照2进制存储的所以他是10011011 并且char是8位(8位是小整形)我们再将他提升成较大的整数类型int(32位)(后面会解释)
所以此时他在内存中这样存储的

00000000 00000000 00000000 10011011

此时我们做位操作

a << 8 //a被升级成int左移8位 

左移后结果为如下所示

00000000 00000000 10011011 00000000 

我们左移31位

a << 31;

10000000 00000000 0000000 00000000
//是原二进制像左移31位

位移运算符满足左结合律
所以
在这里插入图片描述
等同于

((cout << "hi") << " there") << end;

注意我们的位运算的优先级不高不低,比关系运算符优先级高,算数运算符优先级低 所以一下式子如果想先运算关系运算符必须要先打()

cout << (10<42);  
cout << 10 < 42;	//错误这个式子会先计算cout << 10再计算cout<42;  

其实我们的乘法运算也可以用位移运算,
乘以2就是往左位移1位,乘以3就是往左位移一位+原数字(都是以补码运算),比如以下例子

a = 3;  
/*a的补码为00000011(a为正数,正数的补码就是源码)*/  
a*2 = 6;  
/*a乘以2就是补码左移一位就是00000110,为6*/  

我们的cpu计算乘法使用的是布斯算法(booth算法)

4.9 sizeof运算符

sizeof和c语言的sizeof一样返回一个式子,或者一个类型名子所占字节数,sizeof满足右结合率
sizeof所得到的值是一个size_t类型的常量表达式 ,sizeof不会去真正的运行对象的值,他有2种形式

sizeof(type);
sizeof expr;

上面第二种是返回表达式结果类型大小
在这里插入图片描述

我们上面所说sizeof不会去真正的运行对象的值,所以再sizeof *p中就算p是一个无效(未初始化)的指针解引用也是一个安全的操作。

sizeof运算符的结果部分地依赖与其作用的类型
在这里插入图片描述
在这里插入图片描述

4.10逗号运算符

逗号运算符有2个对象依次从左往右运行依次求值
在这里插入图片描述

逗号运算符通常用在for循环中

vector<int>::size_type cnt = ivec.size();
for(vector<int>::size_type ix = 0; ix != ivec.size(); ++ix, --cnt)
{
	ivec[ix] = cnt;
}

以上的表达式中首先vector<int>::size_type cnt(size_type是容器vector<int>的成员类型,一般的他是std::size_type或者unsigned int 或者unsigned long long的别名typedef,我们为什么使用size_of了不直接用int了因为int根据不同的平台而发生大小变化,vector<int>::size_t不会),我们使用size()将容器的对象ivec的元素个数,赋值给cnt(sizeof是求出总字节大小)

4.11类型转换

为什么要类型转换因为,在我们的式子中我们有2个类型有关联比如加减乘数,并且这2个类型不一样,那么为了统一标准就要进行类型转换,比如下面式子

int ival = 3.1415927 + 3;

加法左边是一个浮点数,加法右边是一个整型,在c++中为了不让浮点数的精度缺失,我们一般转换整形成浮点数,参与运算,所以上面的式子3被转换成浮点数再加上左边的浮点数最后得到6.1415927,最后赋给ival,也就是给ival进行初始化,但是ival是int不是double所以6.1415927被转换成6去掉小数点后面的数字(注意计算机不会四舍五入只会去掉小数点就算是6.9415927也被转换成6)
以上进行了2次转换这种转换不需要程序员参与直接转换,他们被称为隐式转换(implicit conversion)
何时发生隐式转换了?
在这里插入图片描述

4.11.1算数转换

算数转换非常的简单我们上一个例子也用到了,就是运算符的运算对象将转换成最宽的类型,比如我们上面的int转换成了double,还有如果一个long double和double做算数运算double将被转换成long double

整型提升
我们的char,bool,signed char,unsigned char,short,unsigned short 等类型被存放再int类型中会被提升成int类型否则提升成unsigned int比如bool的false被转换成0,true被转换成1

在这里插入图片描述

无符号类型的运算对象
我们如果有2个数字做运算,其中先进行整型提升如果2个数字类型都一样那么就转换完成,如果一个是unsigned一个是signed,并且unsigned对象类型大于signed对象类型,则signed转换成unsigned,例如2个类型分别是unsigned和int类型,如果int的值为负值 那么就被转换成unsigned类型,怎么转换非常简单,

可以这样理解
signed int表示范围是  -2147483647~2147483638
unsigend int 范围是0~4294967296 
有符号负数转换成无符号正数可以把以上2个范围求出一个交集就是0~2147483638,再把signed左边的-2147483647~0移到
2147483639~4294967296,让他们重合,比如-42就是4294967296减去42等于4294967254

无符号类型大于有符号类型且有符号类型为负怎么计算已经说了,那么有符号类型大于0并且小于无符号类型了?那就直接相加相减即可
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.11.2其他的隐形转换

在大多数用到数组的表达式中,数组都被转换成指针 比如

int ia [10];
int * ip = ia;

ia被转换成数组,只有当decltype,取地值符(&),sizeof,等运算符的运算对象时,上述的转换不会发生

4.11.3显示转换

显示转换就是我们强制的将对象转换成另外一种类型比如我们下面的式子

int i,j;  
double slope = i/j;  

上面的式子称为强制转换
在这里插入图片描述
强制转换的具体形式如下
cast-name<type>(expression)
其中type是转换的目标,expression是要转换的值,如果type是引用类型,则结果为左值
cast-name有

static_cast
dynamic_cast 
const_cast 
reinterpret_cast

dynamic支持运行时类型识别,我们以后讨论

static_cast

任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast.,

例如我们将j强制转化成double

double slope = static_cast<double>(j) /i;

在这里插入图片描述
我们在进行隐式转换的时候会报一个警告,不过我们使用static_cast并不会,因为我们使用这个操作符的时候就是告诉编译器不在意精度缺失
我们也可以这样玩

void * p = &d;  //正确我们定义一个void指针p(void指针可以存储任何类型非常量的指针) 
double *dp = static_cast<double *>(p)

因为p是空指针,我们再用static强制转换成double类型的指针,值不会变,空指针不会改变其中的值
在这里插入图片描述

const_cast
const_cast只能改变运算对象的底层const,也就是说如果一个对象是底层const我们可以通过const_cast将其转换成非const,也就是拿掉const,但是注意如果转换对象是一个底层const(本身不是一个常量,引用或者指向的对象是一个常量),这是一个合法的去掉const的行为,如果本身就是一个const我们再使用const_cast的时候他是未定义操作

const char *pc ; 
char *p  = const_cast<char *>(pc);	//正确

在这里插入图片描述
记住他不能做类型转换,只能去掉底层const的性质

reinterpret_cast

它可以暴力完成两个完全无关类型的指针之间或指针和数之间的互转,比如用char类型指针指向double值。它对原始对象的位模式提供较低层次上的重新解释(即reinterpret),完全复制二进制比特位到目标对象,转换后的值与原始对象无关但比特位一致,前后无精度损失。

double d = 12.1;
char* p = reinterpret_cast<char*>(&d); // 将d以二进制(位模式)方式解释为char,并赋给*p
double* q = reinterpret_cast<double*>(p);
std::cout << *q; // 12.1

因为转换前后无精度损失,把一个指针转成整数,再把该整数转成原类型的指针,仍可得到原指针值。比如开辟了系统全局的内存空间,需要在多个应用程序之间传递这个空间的指针时,就可以将指针转换成整数值,得到以后再将值转成指针进行对应的操作。但它本质是一个编译期指令,实际动作可能取决于编译器,虽然功能最强但风险最大,且失去了移植性。

reinterpret_cast 运算符并不会改变括号中运算对象的值,而是对该对象从位模式上进行重新解释

一个指向字符串的指针是如何地与一个指向整数的指针或一个指向其他自定义类型对象的指针有所不同呢?从内存需求的观点来说,没有什么不同!它们三个都需要足够的内存(并且是相同大小的内存)来放置一个机器地址。指向不同类型之各指针间的差异,既不在其指针表示法不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的对象类型不同。也就是说,指针类型会教导编译器如何解释某个特定地址中的内存内容及其大小

4.12 运算符优先级表

在这里插入图片描述
在这里插入图片描述

5.0语句

5.1 简单语句

大多数句子都是以分号结尾,我们的一个表达式ival + 5加上一个分号就成为了表达式语句ival + 5;,表达式语句就是执行表达式并且丢弃求值结果,表达式语句中的表达式在求值的时候附带其他的功能,比如赋值和输出结果等

我们最简单的语句就是空语句

;	//这就是一个空语句只有分号没有别的东西

在这里插入图片描述

空语句并不是每时每刻都无害,比如一下式子多加一个分号导致多出了一个空语句,最后程序运行达不到我们想要的效果

while(iter != svec.end());
++ iter;

以上的语句如果iter不等于svec的尾迭代器值那就执行下面的操作就是++iter但是多加了一个;导致多出了一个空语句,那么while为真执行的结果就是空语句,而不是++iter,因为while后面没有;

复合语句
复合语句就是指用花括号括起来的句子,复合语句也成为块(block),一个块就是一个作用域,在作用域里面定义的变量只能在这个作用域内或者这个作用域内的作用域使用

while (val <= 10)
{
	sum +=val;
	++val;
}

此句子如果不加花括号就只能执行一条,以下是空块

while(cin >> s && s != sought)
{}

5.2 语句作用域

我们在if,switch,while,for语句的语句控制结构内定义变量,并且定义的变量只能在其相应语句内部可见,一旦语句结束就超出了作用范围

while (int i = get_num() )
	cout << i << endl;
int i ; //错误,无法在外部访问i

如果其他的代码也想访问此变量,请在外部定义

5.3 条件语句

5.3.1 if语句

if大家都懂c++和c语言的if差不多,语法上condition必须加上括号,注意condition可以是一个表达式,也可以是初始化了的变量声明,但是不管是表达式还是变量声明到最后他的类型必须是能转换成bool类型

if(condition)
	statement;
else 
	statement2; 
//如果condition为真就执行statement,如果condition为假就执行statement2

多if语句

if (condition1)
{
statement1
}
else if (condition2)		//在bash shell里面是elif
{
statement2
}
else if (condition3)	
{
	statement3
}
else if (conditon4)
{
	statement4
}  
else
{
	statement5
}

此时有一个问题请看以下的句子

if(condition)
	if(condition)
		statement;
else 
	statement;

此句子中我们通过缩进看到的想法是如果第一个if为假就执行下面的else,如果为真再去判断里面的if,但是我们的C++规定else与他最近的尚未匹配的if匹配。所以上面的句子再c++看来如下

if(condition)
	if(condition)
		statement;
	else 
		statement;

所以我们的解决方法是加上花括号块

if(condition)
{
	if(condition)
		statement;
}
else 
	statement;
5.3.2 switch语句

switch提供了一条便利的路径使得我们能够在若干固定的选项中做出选择,我们写一个实例程序

unsigned aCnt = 0,eCent = 0, iCent = 0, oCent = 0, uCent = 0;
char ch;  
while(cin >> ch) 
{
	switch (ch) 
	{
		case 'a':
			++aCent;
			break;
		case 'e': 
			++eCent;
			break; 
		case 'i': 
			++iCent; 
			break;
		case 'o':
			++oCent;
			break; 
		case 'u';
			++uCent;
			break;
	}
}

以上是用switch语句写了一个统计元音的小程序
首先定义元音a,e,i,o,u的计数器,分别一个,然后使用switch语句,switch后面跟的是一个表达式,或者是一个初始化的变量声明,将此变量声明后后面的case做比较,如果相等就执行此case的语句,然后按照顺序往下一个一个执行case的语句,直到遇到break,或者到语句结束退出(break是退出switch语句块的不是while语句)。

注意case 标签后面必须是整型表达式,并且case标签不能相同

我们的while(cin >> ch),首先cin从缓冲区输入的是一个字符串他会先去一个字符赋给ch,缓冲区其他的字符不清空,执行完后再取一个

default标签
如果没有任何一个case标签匹配上那么就可以默认匹配default标签

switch (ch) 
{
	case 'a': case 'e': case 'i': case 'o': case 'u':  
		++voweclCnt;
		break;
	default:  
		++otheCnt;
		break;
}

上述的代码case ’a‘到case 'o’标签下面都没有任何语句,代表不管是ch等于a,e,i,o,u都执行++voweclCnt;break;如果都没有匹配到就匹配default标签

switch内部变量定义
我们有可能遇到一个问题就是在switch中case语句里定义变量如果这个case跳过去了没有执行怎么办?c++有此办法,规定所有的case都在一个作用域中,但是你不能初始化他,你只能定义,如果你定义加初始化就会报错,例如

swich(ch)
{
	case a:
		int num=1; //错误
		int sum;	//正确  
		sum = 1; //正确,当我我们不执行case a的时候只会执行int sum,定义变量,不会执行sum=1
	case b;
		sum = 2; //正确,如果跳过case a直接执行case b,sum的确存在,就算明面上不执行case a但是暗地里也会执行case a中的声明操作
}

因为c++不允许控制流跳过初始化直接进入变量作用域。因为有可能你在另一个case里使用这个变量,此时他就没有初始化。 而声明是允许跳过的

如果我们想要此变量只作用与某个case中,我们可以使用此方法,加上{}块

case true: 
{
	string file_name =get_file_name();
}
break;
case false: 
	if(file_name.empty())	//错误 file_name不在此作用域内

5.4 迭代语句

5.4.1 while语句

只要condition为真就一直执行statement(大部分情况是一个块),直到condition为假
格式

while(condition)
	statement;

condition可以是一个表达式,可以是一个带初始化的变量声明

在不确定我们要迭代多少次的时候就用while比较合适

5.4.2 传统for语句

格式

for (init-statement;condition;expression)
	statement

传统的for循环中首先init-statement初始化一个声明,也就是初始化一个变量,然后执行condition条件如果为真就执行下面的statement,执行玩statement后再执行expression,执行完expression后再执行condition条件为真再执行下面的statement,这样一直循环,直到condition条件为假就退出

注意:传统for循环中我们可以省略掉init-statement,condition,expression,但是不能省略他们中间的 ; 注意省略condition代表此处一直就是true

5.4.3 范围for语句

for循环是c++11新标准引入了一个简单的for循环,这种循环可以遍历容器,或者其他序列的所有元素,其语法为

for(declaration:expression)
{
	statement;
}

首先expression是一个序列,比如用花括号括起来的初始值,或者数组,或者vector,string等对象他们的相同点是都可以返回迭代器的begin和end成员,
declaration就是一个变量,他在每次迭代的时候都会被赋予expression中的某个值,我们要确定declaration的类型,最好就用一个auto
statement就是我们执行的语句可以是一个块也可以是一个语句。
当所有的变量遍历完成后就就结束循环,我们写一个范围for循环的例子

vetor<int> v = {0,1,2,3,4,5,6,7,8,9};	//定义一个vector v其内部每一个元素都是int  
for(auto &r:v)	//定义了一个引用r,r以遍历的方式关联vectoe模板v的每一个元素
{
	r *=2;	//通过引用取改变其被引用的值
} 

他的传统for写法如下

for(auto beg = v.begin().end = v.end();beg != end ; ++beg)
{
	auto &r = *beg;
	r *= 2;
}

由此可得范围for循环首先预览了迭代器end,所以我们不难理解为啥给模板加元素的时候不要用范围for,因为当我们给模板加元素的时候,迭代器就失效了而范围for循环必须要用到end迭代器。

5.4.4 do while语句

do while有一个中文名叫做尾部循环,他是先执行循环再判断,不管怎么判断他都会先执行循环语法如下

do
 statement
while(condition)

注意,我们对于dowhile先执行语句块或者语句再执行判断条,所以我们在条件部分定义变量,在语句块里用是非常不合理的

5.5跳转语句

c++ 有4重跳转语句 break,continue,goto,return

5.5.1 break语句

break负则终止离他最近的while,do while,for,switch语句,然后在这些语句后的第一条语句开始执行
break只能出现在循环语句或则switch内部

switch(ch)
{
	case 1:
		...;
		break;
	case 2:
		...;
		...;

}


for(auto ch:v) 
{
	...;
	...;
	break;
}
5.5.2 continue

continue只能出现在循环语句中,表示退出此次循环,开始执行下一次循环。

5.5.3 goto语句

goto语句就是无条件的跳转到同一函数另一条语句,goto的语法

goto label;

label是一个提示符,我们可以无条件的跳转到这个提示符,当然提示符要被定义,提示符如何定义了比如

goto end;   
end: return 0;  //return 0 是一个普通的语句但是前面加上一个标签end,

注意:我们前面switch也提到了,c++不支持跳过带初始化的定义,但是可以跳过一个普通的声明,
还有向后跳跃一个已执行的定义是合法的,如果跳到定义之前代表系统销毁该变量,然后重新创建他比如
在这里插入图片描述
代表销毁了sz重新创建

5.6 try语句块和异常处理

代码中有可能会出现异常,比如连接丢失,输入无效,等等,
在这里插入图片描述

5.6.1 throw表达式

throw表达式用于表示一个异常,其中throw表示的表达式类型为异常类型,通常在stdexcept头文件中,实例

#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
        int ch,ce;
        cin >> ch >> ce;
        if(ch != ce)
                throw runtime_error("data must be same");	//如果ch不等于ce则抛出异常并且输出“data must be same”,然后停止程序
        cout << "equal"<<endl;
        return 0;
}

[root@zhr ~]# ./test               
1 1 
equal
[root@zhr ~]# ./test
1 2
terminate called after throwing an instance of 'std::runtime_error'
  what():  data must be same
Aborted (core dumped)
5.6.2 try语句块

try语句块就是测试语句块内部的句子,catch就是获取try里面的error,这个error是一个变量类型,我们再catch中为这个变量类型赋值然后再执行catch中的语句,如果是多catch那么就执行相对应的catch,就执行最后一个catch语句外面的语句
和switch一样
try语句块格式如下

try
{
	...
	...
}
catch(exception-declaration)
{
	handler-statements
}
catch(exception-declaration)
{
	handler-statements
}
...
...

实例

#include <iostream>
#include <stdexcept>
using namespace std;
int main()
{
        int ch,ce;
        while(cin >> ch  >> ce)	//定义while循环
        {
                try	//开始执行try
                {
                        if(ch != ce)
                                throw runtime_error("sum must be same");	//如果我们输入的ch和ce不相等就定义个个错误,这个错误的类型是runtime_error,我们可以在后面直接写上这个错误输出的说明
                        else
                                break;	//退出最近的一个循环或者switch
                }
                catch (runtime_error err)	//针对上面的runtime_error错误类型,并且定义一个runtime_error类型的变量err
                {
                        cout << err.what()<<"try agin"<<endl;	//调用runtime_error类型变量err的成员函数what()也就是输出我们上面throw后面定义的错误说明

                }

        }
        return 0;
}

在这里插入图片描述

6函数

6.1函数基础

一个函数包括这几部分,从左往右看,首先是返回值,然后是函数名字,然后是一个括号,括号里面是函数形参,最后是语句块也就是函数实体
我们的c++和c的区别是c只能显视的表示形参,而c++可以隐视的表示形参

int main() 	//c++ style
int main(void)	//c style
6.1.1 局部对象

c++中变量有作用域,对象有声明周期
名字的作用域是程序文本的一部分,名字在其中可见
形参函数内部定义的变量局部变量 ,仅在函数内部可见,还有一种变量在所有函数外面创建叫做外部变量

自动对象
自动对象就是存在与块中的定义的对象,当块结束的时候块中的对象就成未定义的值了

局部静态变量
局部静态变量,在第一次执行对象定义语句时初始化,并且直到程序结束时才销毁。
例如

size_t count_call()
{
	static size_t ctr = 0;	//定义局部静态变量和c语言一样
	return ctr++;
}
6.1.2 函数声明

函数和变量一样,我们要先声明在使用,而函数的声明也叫函数原型 其声明格式如下

void print(vector<int>::const_iterator beg,vector<int>::const_iterator end);	
/*vector<int>::const_iterator是容器里的类型表示一个迭代器并且此迭代器只读容器元素,不能改变,而const vector<int>::iterator 表
示此类型是一个迭代器,并且迭代器中的值是只读的*/

以上包含了函数的三要素,返回类型(void) 函数名(print)函数的形参(beg,end)
在这里插入图片描述

6.2.2传引用参数

我们之前学到对于引用的操作其实就是操作被引用的对象,通过引用形参,允许改变一个或者多个实参的值 (和指针差不多,而且c++中建议使用引用实参,不建议用指针),我们可以这样写

void reset(int &i)
{
	i = 0;
}
int main()
{
	int j =42;
	reset(j);
	cout << "j="<<j<<endl;
	return 0;
}

在这里插入图片描述

我们如何让一个函数返回2个数字了?很简单前面返回一个数字,传参的时候,形参定义成为一个引用类型或者指针(c++中建议使用引用),例如如下函数

string::size_type find_char(const string &s , char c , string::size_type &occurs)	//string::size_type是string类型的成员函数size()的返回值,返回字符串长度
{
	auto ret = s.size();
	occurs = 0;  
	for(decltype(ret) i = 0 ; i != s.size() ; ++i)
	{
		if (s[i]  == c )
		{
			if (ret == s.size()) 
				ret = i;
			++occurs;
		}
	}
	return ret;
}

上述的例子中我们用引用解决了需要返回2个数的问题。

6.2.3 const形参和实参

当形参是const的时候我们要注意关于顶层const的情况
我们以前学到的顶层const本身不变,而底层const是指向的值不变
当我的形参是顶层const的时候,实参向这个顶层const形参传递参数也是也以的,但是函数不能改变这个形参的值具体请看下面

#include <iostream>
using namespace std;
void test(const int);	//定义函数
int main()
{
        int i = 0;
        test(i);
        return 0;
}
void test(const int j)
{
        cout << j << endl;
}

[root@zhr ~]# ./test
0
/*当我们调用函数test的时候会传给函数test一个实参i去赋给形参顶层const j,j因为本身不能变,按常理传给j值会报
错,但是在函数调用时候形参的const属性被去掉也就是说i可以赋给j,但是在函数test里赋值给j是非法的*/

在这里插入图片描述

指针或者引用形参与const

int i = 0 ;
const int ci = i;  
string::size_type ctr = 0; 
reset(&i);	//调用的形参是*i
reset(&ci);	//错误不能用顶层const去初始化一个*i,这样就可以通过指针i去改变顶层const了  
reset(i);	//调用形参类型是&i; 
reset(ci);	//错误,不能把普通的引用绑定在顶层const身上,如果绑定成功就可以从引用直接更改顶层const了 
reset(42);	//错误,不能把普通引用绑定在字面上  
reset(ctr);	//错误,类型不匹配ctr是无符号整形  

对顶层const和底层const的理解

首先我们int const *p或者const int *p他们的意思都一样都是底层const,都表示 *p是read-only的,就是不能从 * p去更改对象,但是可以从他赋值的对象去初始化他,
对于顶层const必须要一开始声明就初始化,不能用非常量指针或者引用去初始化他。

尽量使用常量引用
我们的形参尽量使用常量引用(低层const,int const &i),如果不用常量引用那么说明我们可以在函数中通过这个引用去更改实参的值 ,所以如果我们将形参定义为普通的引用,我们不能将字面值,顶层const传给函数。
在这里插入图片描述

6.2.4 数组形参

指针由2个性质对我们定义和使用作用函数有影响分别是

  • 不允许拷贝数组,
  • 使用数组时会将他转换成指针

因为数组不能拷贝,所以不能以值传递的方式去使用数组,还有因为数组会被转换成第一个元素的指针,所以我们在值传递的时候,就是传递的是一个指针
例如我们有一个int类型的数组,我们要把他当成形式参数一下3中函数定义方法都是可行,并且一样的

void print(const int *)  
void print(const int [])
void print(const int [10])

6.4 函数重载

如果一个作用域内几个函数名字相同但是他们的形参列表不相同,沃恩称他为重载函数,
比如我们前面的几个print函数

void print(const char * cp);	//底层const为形参  
void print(const int *beg ,const int *end);	//2个底层const形参
void print(const int ia[],size_t size);

以上我们有3个print函数他们的实参类型不一样,所以我们在使用他的时候编译器根据形参去找相应的函数

typedef phone telno  ;
record lookup(const phone&);
record lookup(const telno&);  
//错误两者形参一模一样

重载和形参
开始我们提到顶层const在形参中顶层const属性将会消失,但是底层const不会,而且底层const和普通函数是2种类型所以

record lookup(phone);
record lookup(const phone); 
//以上2个重复了因为第二行的顶层const性质被去掉了
record lookup(account &);
record lookup(const account &);	//新函数

210

7.0 类

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值