目录
一、C++介绍
oop(面向对象编程)
oop的本质是设计并扩展自己的数据类型。
泛型编程和oop一样都会使的重用代码和抽象通用概念更加简单,
- oop强调的是编程的数据方面
- 泛型强调的是对立于特定的数据类型
类定义描述的是数据格式及用法,对象是根据数据格式规范创建的实体。
类描述指定了可对类对象执行的所有操作,要对特定对象执行这些允许的操作,有两种方法,
- 第一种就是函数的定义
- 第二种就是重新定义运算符
UNIX编译和链接
.c ->.o(目标代码文件)->.out(代码和库文件结合起来形成可执行文件)
Linux编译和链接
预处理,生成预编译文件(.i文件): gcc –E hello.c –o hello.i
编译,生成汇编代码(.s文件):gcc –S hello.i –o hello.s
汇编,生成目标文件(.o文件):gcc –c hello.s –o hello.o
链接,生成可执行文件:gcc hello.o –o hello
注意:
int* p = new int; // 这里面存的地址本文统一称为指针的值 (在别的地方称为指针变量的值)
*p = 4; //这个4本文统一称为指针指向变量的值 (在别的地方称为指针指向的值)
delete(p); //&p 本文称为指针的地址值(在别的地方称为指针地址)//因为我觉得没必要省略一个"的"去显得名词化
二、c++内容简要介绍
函数
1、通常main()被启动代码调用,而启动代码是由编译器加入到程序中的,是程序与操作系统之间的桥梁,可以认为main()是被操作系统调用的
2、c语言中省略int,即仅 main(){.....} 将默认返回类型是int,c++逐渐淘汰了这种做法(事实上可以这么用)。
3、c语言中参数不填意味着对是否接受参数保持沉默,意思是你爱传不传,都能调用。如果参数填void那么意味着不接受任何参数,如果尝试会出现语法错误。
4、只有在main()里面在声明有返回值时可以不return任何东西,不填会默认返回0,其他函数不可以。在操作系统调用main时,return 0一般代表着程序运行成功,非0则一般意味着存在问题。
5、如果返回值是void,那么可以没有return,即使没有返回值,也可以使用return ;来结束程序
6、main函数在整个程序中是必须有的,但一些DLL(动态连接库)可以不需要main方法。
预处理器和头文件
预处理器处理以#开头的编译指令
比如#include <iostream>
使用了include指令,它会将iostream文件包含到程序中。
这是一种典型的预处理器操作:在源代码被编译时,替换或者包含文本
1、iostream是io stream,像这样的文件叫做包含文件,在c语言中通常它们是被包含在其他文件里。
2、头文件名使用扩展名.h,但是在c++中头文件没有扩展名,如果某些头文件是来自c语言,它会在文件名称前面加上 前缀c,比如c++的math.h是cmath。
名称空间
名称空间旨在让编写大型程序时以及组合多个厂商的代码组合时更加容易,就比如两个厂商都有wand()函数,这样使用的时候,编译器不知道用哪一个版本,因此名称空间可以指出想使用哪个厂商的产品。
类、函数和变量便是c++编译器的标准组件,它们现在都放置在名称空间std中。这意味着cout输出函数实际上是std::cout,因此可以直接用std::cout<<"不使用using namespace std;";
如果不使用std空间的全部内容,那么可以只声明自己需要用到的,例如
using std::cout;
using std::endl;
如果有多个函数都是用到了std里面的内容,那么仅需要在使用函数的前面加上using namespace std;
如果仅有一个函数使用到了,则仅在这个函数里面使用
如果仅在一个函数里使用到了std的一个内容,那么可以形如这个使用 std::cout<<"你好";
cout函数
c++提供了用于处理输入和输出的预定义对象cin和cout,它们是istream和ostream类的实例,这两个类是在iostream文件中定义的,cin和cout都是智能对象,能够根据程序上下文将信息自动的从一种形式转换为另一种形式。
cout<<"输出";
<<表示将字符发给cout,cout是一个预定义的对象,知道如何显示字符串、数字等,
<<很像按位左移运算符<<,这是对运算符进行了重载.
cout将整数转换为字符串形式,因此相比c语言更加灵活,这都源自c++运算重载。
endl是特殊的c++符合,它表示重起一行 ,另一个c语言的"\n"也可以实现。
c++源代码风格
花括号各占一行
与函数名相关的圆括号周围没有空格
声明语句和变量
用户定义的函数
在程序中使用用户自定义的函数时,需要提供原型,有两种方法:
- 在源代码文件中输入函数原型
- 包含头文件,在头文件中定义原型
建议使用第二种,更加规范
原型只描述函数接口,但是必须要有的,即使c++也需要拥有,也就是将原型放在main函数之前。
c++简要介绍总结
1、c++程序中的模块叫函数
2、#include <iostream>预处理器编译指令是将iostream文件的内容包含在里面
3、using namespace std的作用是 使的程序能够使用std名称空间中的定义
4、如果在main函数里面使用cout时编译器指出这是个未知标识符,
需要加上声明iostream,需要指明cout用的哪个空间里的
三、c++数据处理
oop的本质就是设计并扩展自己的数据类型,设计自己的数据类型就是让类型和数据匹配。在创建自己的类型前需要了解并理解c++内置的类型,因为这些类型是自己类型的基本组件。
内置的c++类型有两种
- 基本类型(整数、浮点数)
- 复合类型(数组、字符串、指针和结构)(在基本类型上创建的复合类型)
字符
计算机存储字母是通过字母的数值编码解决的,字符集中的字符是用数值编码(ASCII码)表示。
比如输入个M字母放入到ch里面,但事实上ch里面的存放的是77,这种转换操作是由cin和cout自动转换而成的
cout.put()函数显示一个字符
cout是ostream的对象,句号称为成员运算符
在c++的Release2.0之前,
cout将字符变量显示为字符,将字符常量显示为数字
但是c++早期版本,将字符常量存储为int类型,因此‘M’的编码77会被存储在16位或者32位的单元里,但是char变量一般占8位
int a=65; 65是int常量,a是int变量
char a=‘a’ ‘a’是char常量,a是char变量
没看懂表达的什么
字符占用多少等等留待补充
const限定符
建议首字母大写。
创建常量的通用格式是 const type name =value;
const比#define好,它能明确指定类型,并且可以使用c++的作用域以及将const用于更复杂的类型
浮点数
带小数的数字计算机将这样的值分为两部分存储,比如31.1415 和3.11415
第一个数是0.311415(基准值) 和100(缩放因子)
第二个数同样可以表示为0.311415(基准值) 和10000(缩放因子)
缩放因子的作用是移动小数点的位置,只不过它是基于二进制数,因此缩放因子是2的幂不是10的幂
表示法: 可以是正常的1.233 亦可以用e\E表示
浮点数的类型
float、double、lone double 这些类型是根据他们可以表示的有效数位和允许的指数最小范围来描述的,比如1452,有效位就是4,但是1400有效位就是2
浮点常数
float 用f或者F后缀
long double 使用l或L后缀
double类型 E 比如2.45E5
- 初始化和赋值进行的转换 将一个值赋给取值范围更大的类型通常不会导致什么问题,但是
- 以{}方式初始化时进行的转换
强制类型转换
(long) throw //返回throw的long的类型
long(throw) //返回throw的long的类型
强制类型转换不会改变thorn变量本身,而是创建一个新的、指定类型的值
上面第一种格式来自c语言,第二种是存粹的c++,新格式是想让强制类型转换像函数一样调用
四个强制类型转换运算符 (待补充)
强制类型转换后再相加和相加后再强制转换得到的值有时候是不同的,强制类型转换将截短
auto声明
能够让编译器根据初始值的类型推断变量的类型,
auto n=100; //意味着n是int类型的变量
但是auto不是用于简单情况,有些情况需要注意,比如
auto a=0.0 //a是double类型
auto a=0 //a是int类型
auto在处理复杂类型时,比如标准模块库(STL)中的类型时,自动类型推断才会显现出来
(待补充)
三、复合类型
数组
声明
数组声明需要指明
- 存储在每个元素中值的类型
- 数组名
- 数组中的元素数
虽然如书所说元素的数目不能是变量,但实验一下确实可以的。
sizeof运算符返回数据对象的长度,单位是字节
sizeof用于数组名,返回整个数组中的字节数。(a)
用于数组元素,得到的是元素的长度。 (a[1])
因此数组名是数组,数组元素只是一个变量。
数组初始化
使用数组初始化列表对数组初始化。
使用数组初始化列表可以省略数组长度。
int cards[5]={2,36,4}; //可以
int hand[5];
hand[4]={1,2,3,4} //不可以
也不可以 hand={1,2,3,4}
int a[]={1,2,3};//可以
如果对数组初始化时提供的值少于数组的元素个数,那么只初始化前几个,剩下的填默认值。
float a[5]={1.0} 编译器将第一个置为1.0,而将其他的元素的置为0.0
c++11数组初始化方法
1、初始化数组时可省略等号
int a[3] {1,2,3};
2、可不在大括号中包含任何东西,这时候由编译器去设置值
int a[3] {};
3、列表初始化禁止缩窄转换
long plifs[]={25,92,3.0};//浮点数转换为整形式缩窄操作,即使浮点数的小数后面是0
char slifs[4]={'h','i',1122011,'\0'};//1122011超过了char变量的取值范围
char tlifs[4]={'h','i',112,'\0'};//通过 112在char变量的取值范围内
c++标准模板库(STL)提供了一种数组替代品——模板类vector
字符串
字符串可以用char数组,也可以用string。
字符串存储在char数组中时,以空字符结尾,空字符被写作'\0',其ASCII码为0用来标记字符串的结尾
只有第二个是字符串
如果用cout显示cat字符串只会显示前7个字符直到发现空字符就会停止
显示dog会打印前八个字母,并在内存中接着打印直到遇到空字符为止。
字符串-char数组初始化
上面初始化太过于麻烦,可以用字符串常量进行初始化。
字符串常量隐式的包括结尾的空字符,因此不用显式的加上。
为了保证安全,建议使用第二种方式,让编译器计算数组的长度。
但要注意的是,如果真要计算字符串所需的最短数组时,要将结尾的空字符加入在内
比如new一个空间,长度需要strlen+1
双引号(字符串常量)不能与单引号(字符常量)互换,字符常量只是字符串编码的简写表示,‘S’只是83的另一种写法。
"S"不是字符常量,它表示的是两个字符(字符S和\0)组成的字符串,并且"S"实际上是字符串所在的内存地址
char ss=“S”; 企图将一个内存地址赋给ss
字符串常量拼接
c++可以将两个用括号括起的字符串合并为一个。
其实任何两个由空白(这个空白可以是空格、制表符或者换行符)分隔的字符串常量都会自动拼接一个。
事实上第一条语句不加空格也可以
sizeof运算符指出整个数组的长度,strlen()函数返回的数组中的字符串的长度而不是数组本身的长度。strlen会计算空格的,数组的长度不应该小于strlen+1
strlen对于未初始化的字符数组它会寻找'\0',这个可能超出它的范围,因此可能得出的结果比字符数组本身申请的大
字符串输入(读取一个单词)
如何确定已完成字符串的输入呢,cin使用空白(空格、制表符或者换行符)来确定字符串的结束位置,cin在碰到空格后,将字符串放到数组,并自动在结尾中添加空字符。
输入队列中还有内容时,会将内容给下一个cin>>变量。
cin输入字符串比目标数组长,待补充 17章 就下面这个
留下一个疑问,下面如果输入6个值 str2接收不了就会有异常反而影响str1
#include <iostream>
#include <cstring>
main(){
using namespace std;
char name[100];
// cout<<"aaa bb cc" "aaa dd ";
// char str1[]="nihao";
// cout<<"str1用strlen测量的长度是 "<<strlen(str1)<<endl;
// cout<<"str1用sizeof测量的长度是 "<<sizeof(str1)<<endl;
// char str2[]="nih ao aa";
// cout<<"str2用strlen测量的长度是 "<<strlen(str2)<<endl;
// cout<<"str2用sizeof测量的长度是 "<<sizeof(str2)<<endl;
char str1[]="aaaa";
str1[3]='\0';
char str2[5];
char str3[5];
cin>>str2;
cin>>str3;
cout<<"str1 "<<str1<<" str2 "<<str2<<" str3 "<<str3;
}
void test1()
{
using namespace std;
cout<<"h";
}
字符串输入(读取一行)
getline(),第一个参数是数组名称,第二个是要读取的字符数,如果参数是20,最多读取19个
getline在碰到换行符或者遇到读取指定数目的字符后自动停止读取
cin.getline(str1,20);
在碰到换行符以后用'\0'替代(但不会将换行从缓存区中拿走)。
cin.get(str,20)
cin.get(str,20)不读取并丢弃换行符,将其留在输入队列中,如果连续两次调用get(),那么第一次调用后,换行符就会留在队列中,因此第二次调用时会直接停止,因为它看到的第一个字符就是换行符。
如果想要处理的话,可以用不带任何参数的cin.get(),它会处理读取下一个字符(即使是换行符)
因此在连续调用cin.get,可以在两个之间加上一个不带参数的 get
推荐使用后者,因为前面getline不知道是读了一行还是数组已经填满,对于get可以通过判断查看下一个输入字符,如果是换行符,说明已经读取了整行,否则说明还有其他输入。
cin.get和cin.getline还有阻断机制,看下面的内容。
输入缓存区
cin在碰到回车停止读取后,会把回车留在缓存区里面,如果这时候有getline那么直接碰到回车停止。这时候用cin.get(c)处理cin丢弃的换行符之后getline就可以正常等待用户输入
cin.getline(str,5)同样不会丢弃回车,因此会影响接下来的输入。
getline(cin,str)遇到换行符后会丢弃这个换行符,因此不会影响接下来的输入。
需要注意的是 getline(cin,str)的str是string str;
总结:
1、cin对于回车会留在缓存区
2、cin.getline(str,5)对于回车会留在缓存区
3、getline(cin,str)会丢弃回车
混合输入字符串和数字
cin以后使用cin.getline将会在第二次读取一行时失效
解决方法:1、使用cin后加入一行cin.get()
2、拼接 (cin>>year).get() 将去掉那个空格
失效阻断机制
#include <iostream>
#include <cstring>
main(){
using namespace std;
char name[100];
char str1[5];
string str5;
cin.getline(str1,5);
// getline(cin,str5);
char a[5];
// a=getchar();
cin>>a;
cout<<"str1-"<<str1<<endl;
cout<<"a-"<<a<<"-"<<endl;
getchar();
char b[6];
cin>>b;
cout<<"b"<<b;
}
如果输入123456,由于str1只要4个,那么cin无法得到剩下的56
这是因为输入阻断。
1、使用cin.get()遇到空行,那么接下来的输入将会被阻断,也就是不管后面有多少希望输入的语句都得不到内容
2、使用cin.getline()输入字符串比分配的空间长,那么会把余下的字符留在输入队列中,并设置失效位,并关闭后面的输入
解决方法
可以使用cin.clear() 对于第二种情况,它将清空后面的字符
string类
要使用string需要包含头文件<string>,string类位于std名称空间中。
string可以使用cin输入存储到string对象中
可以用cout来显示string对象
可以使用数组表示法来访问存储在string对象中的字符
因此可以将string看做一个简单变量
string str1;
cin>>str1; //可以看到没有指明str1的长度
c++11初始化
char数组和string区别和关联
char数组不能直接赋值给另一个数组,但string可以
char ch1[20]="aaa";
char ch2[20];
string str1="aaaa";
string str2;
ch2=ch1 //非法
str2=str1 //合法
string合并可以直接+=
对于字符数组赋值复制操作,可以使用c语言库中的函数完成任务,头文件cstring提供了这些函数
字符数组之间的复制
strcpy(charr1,charr2); //char2给charr1赋值
字符数组附加到末尾 //charr2附加到charr1
strcat(charr1,charr2);
字符串的比较
c-风格的
使用strcpm()进行比较 ,等于0则相等
有些语言,存储在不同长度的字符串彼此不相等,但是c++不这样
不能用关系运算符来比较字符串,但是可以用来比较字符。因为字符是整型
那么就可以自己写个方法去逐个去比较
string类字符串
类函数重载了关系运算符,因此可以用它来比较字符串
raw原始字符串
raw原始字符串,在原始字符串中,字符表示的就是它自己,而无需再进行转义
需要用R进行标识原始字符串
如果想要输入)
结构
如果想要用string类型,需要将using namespace std;放在结构定义之前
下面是一个没有名字的结构,它忽略了名称,同时定义了一个结构和这么一个类型的变量。但以后就不能创建这种类型的变量了。访问是可以使用position.x进行访问。
结构声明
前一种是c语言要求的,需要有struct,第二种是c++要求的,可以省略struct
结构初始化方式
也可以全部放在一行
c++11结构初始化
如果大括号没有加任何东西,那么都将被初始化为0
不允许缩窄转换
结构数组
创建
使用
结构初始化 声明两个对象,然后对每一个都进行定义
共同体
匿名共同体
枚举
此时spectrum成为新类型的名称,spectrum被称为枚举
声明
spectrum band;
指针
int * p; int* p; int*p都是可以的
声明了一个指针,将指针里存储的地址指的内容修改为223323。但是这个地址是啥呢?因此
一定要对指针应用解除引用运算符(*)之前将指针初始化为一个确定的、适当的地址(使用指针的金科玉律)
指针和数字
指针不是整型,虽然一般将地址作为整数来存储,但是将整数赋给指针将会出错
出错原因是类型不匹配,可以通过强制类型转换来完成操作
变量是在编译时分配的有名称的内存,指针是通过名称直接访问的内存提供别名,指针真正的用处是在运行阶段分配未命名的内存以存储值。
为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式
比如 int *pt =new int;
变量的值都存储在栈中,new是从堆或自由存储区的内存区域分配内存。
c++值为0的指针被称为空指针,c++确保空指针不会指向有效的数据,因此它通常用来表示运算符或函数失败(因为如果成功它们将返回一个有用的指针)。
delete pt释放内存
上面表示的是释放pt指向的内存,但不会删除指针pt本身,因此可以将pt重新指向另一块新分配的内存块。
此外不是尝试释放已经释放的内存块。
静态联编和动态联编
对于小型的数据对象可以申明一个简单变量,对于其他比较大型的数据(如数组、字符串和结构)应使用new,如果要编写一个程序,它是否需要数组取决于运行时用户提供的信息,如果仅仅只通过声明创建数组,那么在不管程序最终是否使用数组,数组都占用了内存。在编译时给数组分配内存被称为静态联编。如果在运行阶段需要数组则创建,如果不需要则不创建,这称为动态联编。
使用new创建动态数组
int * psome=new int [10]; //new 运算符返回第一个元素的地址,这个地址被赋给指针psome
delete [] psome; //释放new创建的数组 这里需要加[],具体看内存管理那个博客
psome[1] //第二个元素
可以看到将指针当做数组名使用即可。
但是* psome;//第一个元素 int四个字节,那么*(psome+=4)便是第二个元素???
不是的,+1是第二个元素。
指针变量+1增加的量等于它指向的类型的字节数,对于指向double的指针+1后,数值将会增加8
c++将数组名解释为地址 也即数组第一个元素的地址。
double * pw=wa;//wa为一个数组
double * ps =&wa[0] ; //wa首元素和数组名实际上是一个概念
数组名不能+1,数组名是常量
指针可以+1
对数组应用sizeof得到的是数组的长度,对指针是指针的长度(即使指针指向的是一个数组)
对数组取地址时,数组名不会被解释为其地址而是被解释为第一个元素的地址,对数组名应用地址运算符(&)时得到的是整个数组的地址。
虽然数字上两个地址相同,但是概念上&tell[0](即tell)是一个2字节内存块的地址,而&tellshi yige 20字节内存块的地址,因此tell+1是将地址值加2(short的字节数),而表示式&tell+2是将地址加20
tell是一个short指针(*short),而&tell是指向包含10个元素的short数组指针,虽然数值相同。
上面那句话意思是pas是一个指针,指向拥有20个short元素数组的指针,等号它将tell整个数组地址赋给了这个指针。那么使用的时候本身pas是指向整个数组,那么(*pas)[0]便是数组的第一个元素。
const char * bird="e123456";
"e"实际表示的是字符串的地址,一般来说编译器在内存中留出一些空间用来存储用引号括起来的字符串,并且将每个被存储的字符串与其地址关联起来
而字符串字面值是常量,因此需要用const,如果不加会报错。
cout<<bird;//会输出e123456
但如果cout<<*bird;//只会输出e 留待补充
如果给cout提供一个指针,它将打印地址,但如果指针的类型是char *,则cout将显示指向的字符串,如果想要显示地址,那么必须将这种指针强制转换成另一种指针类型,比如int *
char animal[20]="brea";
char * ps =animal; //cout<<ps 将会输出brea; cout<<(int * )ps 将会输出地址
这里ps和animal指针都指向brea这个常量
如果想要 单独获得字符串的副本,可以通过ps=new char[strlen(animal)+1]首先完成获取内存,随后通过strcpy(ps,animal);将内容复制;
使用new创建动态结构时,不能将成员运算符句点用于结构名,因为这种结构没有名称,只是知道它的地址。
由于pt是指向结构的指针,那么*pt就是被指向的值结构本身,那么*pt就是一个结构,因此也可以使用(*pt).good进行访问。
总结
1、char * 指针可以指向静态字符串,但需要在前面加上const,这表示的是这个指针指向了那个静态字符串的地址,如果以后修改将修改那块内存的里的数据。
const char * p=“aaa”;
p="cccc";
cout<<p;
2、指针里面存放的是地址,打印将会输出一个地址,但如果指针是char *类型的,那么打印指向的字符串。
3、 指针存的内存块的地址可以是一个变量的地址(自动存储),也可以是new的一块地址。
自动存储、静态存储和动态存储
自动存储
在函数内部定义的常规变量使用自动存储空间,被称为自动变量,这意味着在所属的函数被调用时自动产生,在函数结束时消亡。如果程序控制权由一个子程序回到主程序,那么子程序里的内存将自动被释放,当然如果子程序返回给主程序某个值,这个值的内存
自动变量存储在栈中,执行代码时其中的变量依次加入栈中,而在离开代码块时,将按相反的顺序释放这些变量--后进先出,因此程序执行过程中,栈将不断地增大和缩小。
静态存储
静态存储是整个程序执行期间都存在的存储方式,使变量称为静态有两种方式:
1、在函数外面定义它
2、在声明变量时使用关键字static
待补充
动态存储
那便是new和delete运算符,它们管理了一个内存池,这被称为自由存储空间或堆。
new和delete能够在一个函数中分配内存,而在另一个函数中释放它(比如一个函数里new了一块,用指针指向这一块,返回给主程序里的某个指针)。
数据的生命周期不完全受程序或函数的生存时间控制,让程序员对使用内存有了更大的控制权然而内存管理也负责了,在栈中,自动添加和删除机制使的占用的内存总是连续的,但new和delete占用的自由存储区不一定连续。
二维数组
由数组组成的数组
二维数组初始化
const char * p=“aaa”;指针指向一个字符串
那么指向一个字符串数组 const char *a[4]={{},{},{},{}}
char数组的数组,如果想要字符串是可修改的,那么应该省略限定符const
有2个char数组
string对象数组
可以看到char * 数组和string对象数组更加简单,并且能够更加省空间。
栈、堆和内存泄露
有过new运算符在堆上创建变量后没有调用delete,即使包含指针的内存由于作用域规则和对象生命周期的原因而释放,在堆动态分配的变量或结构也将继续存在。且由于无法访问将会导致内存泄露
vector模板类(容器)
这两个容器都可以使用sort(开始地方,结束地方)进行排序,需要加上头文件#include<algorithm>
vector类使用new和delete来管理内存,但这些工作是自动完成的。
使用vector需要包含头文件vector,vector包含在名称空间std中
会自动调整长度,但也可以直接指明
如果指明长度,将会全部被初始化对应的类型,这点需要注意。
array模板类
解决 vector效率稍低,同时可以长度固定且方便和安全
array对象长度固定,使用栈(静态内存分配)
n_elem不能是变量
vector和array对象
会出现越界,但是编译器不检查,因为额外检查的代价是运行时间长,但可以用at()捕获非法索引。
循环和关系表达式
for循环
c++11 基于范围的for循环
适用:对数组(或容器类,如vector和array)每个元素执行相同操作
但是我发现如果没有指明数组长度有多少,那么就没法用下面的for循环,就比如将一个数组传递给一个函数,这时候不知道数组有多少东西,还有一种可能是传递给函数,函数接收的只是一个指针,并不认为它是一个函数。
1、不对数组元素修改
2、需要数组里的元素修改
符号&表示x是一个引用变量
3、其他形式
while循环
cin.get(ch)读取下一个字符
cin.get(ch)去改变ch这个变量的值,但是在c语言中需要把ch变量的地址进行传递
在c++中只需要把函数将参数声明为引用即可。引用的知识点以后会更新。
eof
检测到eof后,cin将eofbit和failbit都设置为1,可以通过eof()成员函数查看eofbit是否被设置,如果设置后,将返回true,或者使用fail()成员函数查看,这两个函数都是报告最近读取的结果,也就是它们是读取以后再报告。
#include <iostream>
main(){
using namespace std;
char name[100];
char ch;
int count=0;
cin.get(ch);
while(cin.fail()==false)
{
++count;
cin.get(ch);
}
cout<<count;
}
ctrl+z需要单独按 输入上再回车 ctrl+z不能和其他的字符在一块
char ch;是只能存放一个字符,10是两个字符
cin.get(ch)的返回值,当cin出现在需要bool值的地方,istream会调用一个将istream对象转换为bool值的函数,因此下面这个是精简版,不需要在while之前来个cin.get(ch)
#include <iostream>
#include <cstring>
#include <fstream>
int main()
{
using namespace std;
string str;
cin>>str;
char ch;
while(cin.get(ch))
{
str+=ch;
}
cout<<str;
}
输入文字到文件中 按ctrl+z结束
#include <iostream>
#include <cstring>
#include <fstream>
int main()
{
using namespace std;
char ch;
ofstream ofFilename;
ofFilename.open("text.txt");
while(cin.get(ch))
{
ofFilename<<ch;
}
ofFilename.close();
}
需要注意的是,如果此次cin后后面还有需要输入的地方,需要清空cin,使用cin.clear()
#include <iostream>
#include<string>
using namespace std;
void main()
{
string strList[5];
for (string & a10 : strList)
{
char ch10;
while (cin.get(ch10))
{
a10 += ch10;
}
cin.clear();
}
for (string a11 : strList)
{
cout << "----------------------------------" << endl;
cout << a11 << endl;
}
}
用cin.get()判断是否是eof
使用cin.get() 这个是返回一个字符
cin.get(ch)这个是返回一个对象。
使用EOF,当函数到达EOF时,cin.get()返回EOF,用一个符号常量表示这一个特殊值,通常EOF被定义为值-1,由于没有ASCII码为-1的字符的值,但如果是返回-1的话,这便是两个字符所以需要将字符转换为int类型判断。
分支语句和逻辑运算符
if else if else结构
if else本身就是一条语句,那么
字符函数库 cctype
isalpha(ch)判断ch是否是字母
ispunct(ch)判断ch是否是标点符号
isdigits(ch)判断ch是否是数字
isspace(ch)判断ch是否是空格
ispunct(ch)判断ch是否是标点符号
?运算符
switch
将枚举应用于switch,switch将int值和枚举量标签进行比较时会将枚举量提升为int。另外在while循环中也会将梅菊亮提升为int类型。
break和continue
break会跳出循环,continue会停止本次循环继续下一次的循环。
简单文件输入输出
文件输入
控制台输出
- 必须包含头文件iostream
- 头文件iostream定义了一个处理输出的ostream类
- 头文件iostream声明了一个名为cout的ostream对象
- 必须指明名称空间std;
- 可以结合使用cout和运算符<<来显示各种类型的数据。
文件输出
- 必须包含头文件fstream
- 头文件fstream定义了用于处理输出的ofstream类
- 必须自己声明一个或多个ofstream对象。
- 必须指明名称空间std
- 需要将ofstream对象与文件关联起来-----方法之一是使用open方法
- 使用完文件后应使用方法close()将其关闭
- 可结合使用ofstream对象和运算符<<来输出各种类型的数据
可以看到区别就是控制台输出对象cout已经定义了,但是文件输出的对象还没有定义,需要自己定义并和文件关联起来。
一旦对象与文件关联起来,那么对象就能像cout一样操作输出了(cout<< ;endl;setf())
ofstream outfile;
outfile.open("fish.txt");
char filename[50];
cin>>filename;
fout.open(filename);
因而
程序使用完文件后,应当把这个对象.close(); 这里不加参数的原因是对象已经和文件关联起来了(如果忘记关闭文件,程序正常终止时会自动关闭它)
将一个现有文件的长度截断为len。如果以前文件长度大于len,超过len的部分将不能再访问。
长度截断为0相当于将文件内的数据全部删除(换个表达就是清空文件里的内容)。
#include <iostream>
#include <cstring>
#include <fstream>
int main()
{
using namespace std;
char ch;
ofstream ofFilename;
ofFilename.open("text.txt");
while(cin.get(ch))
{
ofFilename<<ch;
}
ofFilename.close();
}
文本输出
控制台输出
- 必须包含头文件iostream
- 头文件iostream定义了一个处理输入的istream类
- 头文件iostream声明了一个名为cin的istream对象
- 必须指明名称空间std
- 可以结合使用cin和运算符>>来读取各种类型的数据
- 可以使用cin和get()方法来读取一个字符
- 可以使用cin和getline()来读取一行字符
- 可以结合使用cin和eof()、fail()方法来判断输入是否成功
- 对象cin本身被用作测试条件时,如果最后一个读取操作成功,它将被转换为布尔值true,否则false
从文件中输出
good()函数
这个函数指出最后一次读取输入的操作是否成功,
一种标准方法是在循环前放置一条输入,在循环的末尾放置另一条输入语句
inFile>>value的结果为inFile,在需要bool的情况下,它将转换成true或false
因此两条用一条替代。
#include <iostream>
#include <cstring>
#include <fstream>
int main()
{
using namespace std;
char ch;
ifstream inFile;
inFile.open("text.txt");
if(!inFile.is_open())
{
exit(EXIT_FAILURE);
}
while (inFile.get(ch))
{
cout<<ch;
}
}
函数
函数原型
原型告诉编译器函数名、参数类型、参数个数等,然后在某个调用的时候检查是否符合原型,而为了提高效率所以并没有在文件中查找,而且函数有时候甚至不在文件中(c++允许将一个程序放在多个文件中,并单独编译这些文件并将它们组合起来),在这种情况下,编译器在编译main()时可能无权访问函数代码。
想要不写原型,那么就需要在使用这个函数之前定义它。
参数
当传递多个参数,如果两个参数类型相同,也必须都写类型
函数和数组
方括号为空表明可以将任何长度的数组传递给函数,但arr实际上并不是数组而是一个指针,但在函数的其它部分可以将arr看作是数组。
c++将数组名解释为第一个元素的地址即cookies==&cookies[0];
但是有一些例外,例如
- 数组声明使用数组名来标记存储位置,
- 对数组名使用sizeof将得到整个数组的长度(不包含前面和后面的一些信息所占的内存)
- 将地址运算符&用于数组名时返回整个数组的地址
后面两条得出的数一样但是意义不一样。
在调用函数的时候,传递的是数组名。因为是数组名是第一个元素的地址,所以参数的类型必须是int指针,即int *;那么下面这个同样也可以
但是需要注意的是,当且仅当用于函数头或者函数原型时 int *arr和int arr[]的含义才是相同的,此时他们都意味着arr是一个指针。在其他地方int arr[]提醒着,arr仅是指向int数组的第一个int,而int *arr 仅是一个独立的值。
#include <iostream>
#include <cstring>
using namespace std;
void fun(int a1[],int count)
{
cout <<"fun函数里a1的长度是"<< sizeof(a1);
/*for (int i=0;i<count;i++)
{
cout << c1;
}*/
}
int main()
{
int a[4] = { 0,1,2,3 };
int c = 4;
int* a5;
cout << "main()中指针的长度是" << sizeof(a5) << endl;
cout <<"main()中c的长度是"<< sizeof(c)<<endl;
cout << "main()中a的长度是" << sizeof(a) << endl;
fun(a, 4);
}
可以看到函数里查看sizeof(数组名)确实是一个指针长度的数量,也就是函数里的数组参数确实是一个指针。
希望能记住下面两个恒等式
将指针(包括数组名)加1,实际上是加上了一个与指针指向的类型的长度相等的值。对于遍历数组而言,使用指针加法和数组下标时等效。
回到主线,将数组的第一个元素的地址传递给了函数,函数将第一个元素的地址赋给了指针变量arr,因此并没有将数组内容传递给函数,但是函数可以使用原来的数组。传递常规变量时,函数使用变量的拷贝,传递数组时,函数使用原来的数组。
数组名与指针对应可以节省复制整个数组所需的时间和内存,但是增加了破坏原始数据的风险。但是const限定符解决了这个问题。
传递数组,想从数组的后几个传,可以
&cookies[4]
或者 cookiest+4 这里的4是4个int
注意上面这种用法,一会有个传递两个指针的需要用到它
如果仅仅是想要传递数组的值,不想改变数组里的值,可以应const来限定。
这里的const是在被调用的函数里的参数列表中声明这个不能动,并不是在main里面说明不能动
在调用这个含有const的函数后,main里面照样还是可以修改整个数组。
#include <iostream>
#include <cstring>
#include <fstream>
void fun1(int a[])
{
a[1]=1;
a[2]=2;
a[3]=3;
}
void fun2(const int a[])
{
std::cout<<a[2];
}
int main()
{
using namespace std;
int a[3];
fun2(a);
fun1(a);
cout<<a[2]; //输出2
}
因此
从上面可以看到,对于处理c++的函数,必须将数组中的数据种类、数组的起始位置和数组中元素的个数提交给它---------------指向数组起始处的指针是第一个参数(提供数组位置和数据类型),数组长度作为第二个参数(提供长度)。
还有一种方法是传递两个指针,第一个指针指向数组开头,,第二个指针指向数组结尾。
这里提供传递两个指针的例子
这里看到加了一个常量指针,是由于仅是看数据没有想修改数据的意图,因此需要施加限定。
指针和const
1、常量指针 ——防止使用指针来修改指向的值
int age=30;
const int * p=&age;
2、指针常量——防止改变指针指向的位置
int * const p=&age;
在哪里声明代表的在哪里不能修改,如果本身人家没有加,你加了,你不能修改,但是别的地方可以修改,此为c++禁止将const地址赋给非const指针,虽然可以通过const_cast突破这种限制。
尽可能的使用const
函数和二维数组
调用
int a[][4] = { {1,2,3,4},{2,4,5,6},{4,5,8,7} };
fun2(a,3);
那么fun2的声明怎么声明呢
1、void fun2(int (*pa)[4],int size)
2、void fun3(int pa[][4], int size)
可以看出 pa是指针不是数组,由于指定了列数,那么只能接受由4列组成的数组。
使用
第一种方法 pa[i][j]
第二种方法 *(*(pa+r)+c)
还有一种方式,pa[1]是由4个int组成的数组的名称(强调:pa[1]数组的名),那么访问pa[1]数组的下标为1的元素应该+1,
pa[1]表示为*(pa+1) (强调:这里*(pa+1)是数组名)
*(pa+1)数组的下标为1的元素 *(pa+1)+1
拿到它 *(*(pa + 1) + 1)
函数传递和返回c_风格字符串
c_表示字符串的方式
- char数组 char a[10]="abcdefg";
- 字符串常量 "abcdefg";
- char类型的指针 char * str="abcdefg";
传递
传递的时候 直接 strlen(a)、strlen("abcdefg")、strlen(str);
这里的传递,实际上都是传递的字符串的第一个字符的地址
上面说过,c_风格的字符串与常规char数组之间的区别是
字符串有内置的结束字符,因此不需要将字符串长度作为参数传递。
空值字符'\0'的数字编码是0
返回
建立一个指向char类型的指针,接收时也是用一个指向char类型的指针接收。
函数传递结构
可以将一个结构赋给另外一个结构,但是与指针不同的是结构名只是结构的名称,要获得结构的地址,必须使用地址运算符&。
有三种方式
- 按值传递
- 引用传递
可以直接将结构作为参数传递,并在需要的时候将结构作为返回值返回,但是按值传递结构有一个缺点,如果结构非常大,复制结构将增加内存要求,
直接传递
#include <iostream>
using namespace std;
struct St
{
int StA;
int StB;
};
St fun1(St a, St b)
{
St c;
c.StA = a.StA + b.StA;
c.StB = a.StB + b.StB;
return c;
}
void main()
{
St a = { 1,3 };
St b = { 3,4 };
St c = fun1(a,b);
cout << "c的sta是" << c.StA << " c的stb是" << c.StB << endl;
}
传递结构的地址
传递结构的地址,被调用的函数使用指针来接收
结构*p=&结构
可以看到在函数里面,a需要->符号
此外如果不想改变a的内容,那么fun2应该改为 void fun2(const St *a);
函数和string
可以看到将字符串数组传递会影响字符串的值。
但是需要注意的是传递字符串,当字符串被修改时不会影响字符串的值
函数和array对象
类对象是基于结构的,因此结构编程方面也适用于类,可按值将对象传递给函数
std::array<double,4)> expenses; //使用一个array数组来存储一年的开支
show(expenses); //值传递——————————适用不需要修改
fill(&expenses); //将对象的地址传递 ————适用修改数据
声明
void show(std::array<double,4> da);
void fill(std::array<double,4> *da);
函数内使用
da[3];
*da[3];
模板array并非只能存储基本数据类型也可以存储类对象。
递归
1、包含一个函数调用的递归
通常的方法是将递归调用放在if语句中
可以看到当if满足的时候,它就会一直重复调用
--------------------------------冲--------------(符合if)---------------------------------------------------------->
当到达不满足if的时候,它执行完就会往回退
<--------------------------------退-----------------(执行else和剩余的语句)-----------------------------------
2、包含多个递归调用的递归
如果递归层次较少,是一个简单的选择。
如果多层调用会导致递归层次很多。
函数指针
函数也有地址,函数的地址是存储机器语言代码的内存的开始地址。
函数指针的用处:
可以编写将另一个函数的地址作为参数的函数,这样第一个函数就能够找到第二个函数并运行它。它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。
使用函数指针:
- 获取函数的地址
- 声明一个函数指针
- 使用函数指针来调用函数
1、获取函数的地址
使用函数名(不能加参数),需要区分传递的是函数的地址还是函数的返回值。
void think(){.......}
process(think);// 这里使的在process函数能够在内部调用think()函数
thought(think()); //需要注意的是这里仅是将think的返回值传递给函数,并不是传递的函数
2、声明函数指针
声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。也就是说,声明应像函数原型那样指出有关函数的信息。
在声明时需要注意的是
int (*pf)(int); //这是函数指针,int是传递函数的返回值,最后一个int是参数
int *pf(int); //由于优先级的问题,说明pf(int)是一个函数,返回值为int *指针
调用
pf(1); //这种形式可以
(*pf)(1);//这种形式同样可以。
#include <iostream>
using namespace std;
int getcount(int a1, int a2)
{
return a1 + a2;
}
void func(int a1,int a2,int (*pFunc)(int ,int ))
{
cout << pFunc(a1,a2);
}
void main()
{
func(1,2,getcount);
}
函数指针数组
稍微复杂点的,假设有三个函数,函数的原型如下
虽然特征标看似不同,但实际上这三个都是一个意思。
这里说一下指针的指针
在说明指针的指针时需要将前面的类型加上 再加上*
比如上面p是int * ,我q就应该(int * )*,当然这个()是不能加的(如下图),只是说明想要指向某个变量,就应该把变量的类型加上,后面再加个*
回归主线,f1是下面这种类型,
那么我想一个指针指向它,就应该把f1自己的定义留下,把f1这个名称所在的地方挖了,然后填上一个* p1去指向它,也就是下面。
c++11增加auto关键字,它可以自动判断类型,那么可以用下面这样。
它实际上是表达的
这时候我想调用这个指向函数指针的函数
根据前面说的,想要调用指向函数的指针,参照以前说的,也就是下面这些内容
pf(1); //这种形式可以
(*pf)(1);//这种形式同样可以。
我想要调用可以(*p1)(av,3)也可以p2(av,3)
当然这里指的是f2地址,那么上面调用只会显示函数的地址
正常调用指针函数
#include <iostream>
using namespace std;
const int* f1(const int ar[], int n);
const int* f2(const int [], int n);
const int* f3(const int *, int n);
void main()
{
int av[3] = { 1,2,3 };
int av2[3] = { 7,8,9 };
//声明一个指针指向一个函数
const int * (*p1)(const int*, int) = f1;
auto p2 = f2; //c++11 新加类型
cout << "使用了函数指针类型\n";
cout << (*p1)(av, 3) << ": " << *(*p1)(av, 3) << endl;
cout << &av2[0] << endl;
cout << (*p1)(av2, 3) << ": " << *(*p1)(av2, 3) << endl;
cout << p2(av, 3) << ": " << *(*p2)(av, 3) << endl;
const int* (*pa[3])(const int*, int) = { f1,f2,f3 };
auto pb = pa;
}
const int* f1(const int *ar, int n)
{
return ar;
}
const int* f2(const int ar [], int n)
{
return ar + 1;
}
const int* f3(const int ar [], int n)
{
return ar + 2;
}
这个例子是正常的调用指针函数
array与指针函数
代码往后翻
下面这个是判断类型,其实pb1和pb2类型是一样的
不想看了 待补
部分代码
#include <iostream>
using namespace std;
const int* f1(const int ar[], int n);
const int* f2(const int [], int n);
const int* f3(const int *, int n);
void main()
{
int av[3] = { 1,2,3 };
int av2[3] = { 7,8,9 };
//声明一个指针指向一个函数
const int * (*p1)(const int*, int) = f1;
auto p2 = f2; //c++11 新加类型
cout << "使用了函数指针类型\n";
cout << (*p1)(av, 3) << ": " << *(*p1)(av, 3) << endl;
cout << &av2[0] << endl;
cout << (*p1)(av2, 3) << ": " << *(*p1)(av2, 3) << endl;
cout << p2(av, 3) << ": " << *(*p2)(av, 3) << endl;
const int* (*pa[3])(const int*, int) = { f1,f2,f3 };
auto pb = pa;
//使用指针array
const int* (*pb1[3])(const int*, int) = {f1,f2,f3};
auto pb2 = pb1; //使用pb2指向pb1
cout << "使用了指针array\n";
for (int i = 0; i < 3; i++)
{
cout << pb1[i](av, 3) << ": " << *pb1[i](av, 3) << endl;
}
cout << "使用了指针array\n";
for (int i = 0; i < 3; i++)
{
cout << pb2[i](av, 3) << ": " << *pb2[i](av, 3) << endl;
}
cout << "使用了指向array的指针\n";
auto pc = &pb1;
//输出第一个元素
cout << (*pc)[0](av, 3) << " : " << *(*pc)[0](av, 3) << endl;
}
const int* f1(const int *ar, int n)
{
return ar;
}
const int* f2(const int ar [], int n)
{
return ar + 1;
}
const int* f3(const int ar [], int n)
{
return ar + 2;
}
上面我们用到auto,还可以使用typedef进行简化。
函数探幽
1、C++内联函数
1)函数执行过程
2)内联函数的使用
- 在函数声明前加上关键字inline
- 在函数定义前加上关键字inline
以上两种都可以。但是需要注意的是程序员将函数作为内联函数的时候,编译器不一定会满足这个要求,可能会因为函数过大或者形成了递归。
3)内联与宏
#define squ(x) x*x
a=squ(5);
这不是传递参数实现的,而是通过文本替换实现的。
2、引用变量
1)引用变量声明
引用是一个变量的别名
它的用途是用作函数的形参,通过将引用变量用作参数,函数将使用原始数据。
int rats;
int & rodents = rats;
&不是地址运算符,而是类型标识符的一部分。上述引用声明允许将rats和rodents互换,因为它们指向相同的值和内存单元。&称为左值引用,还有&&称为右值引用,待补
类型后面不可能加&,如果加了则后面的变量就是=后变量的别名
注意!!必须在声明引用时将其初始化,而不能像指针那样先声明后赋值。
上述声明实际上是下述代码的伪装表示:
int * const pr=&rats;
可以看到const更像一个指针常量;
2)将引用用作函数参数
void swapr(int &a,int &b)
3)引用的属性和特殊使用
如果不想对信息进行修改,还想要用引用可以使用常量引用
double refcube(const double &a);
这样可能有点傻,但是这个在数据比较大(如结构和类)的时候,引用参数会很有用。
4)将引用用于结构上
与一般的引用一样,这里不加赘述。
但是如果对结构进行了修改
#include <iostream>
#include <string>
using namespace std;
struct Student
{
int nums;
string name;
};
Student& UpdateNums(Student & const stu)
{
cout << stu.name;
cout << stu.nums<<endl;
stu.name = "小明";
return stu;
}
void main()
{
Student stu1 = { 4,"李明" };
UpdateNums(stu1);
cout << stu1.name;
}
对于这个程序,返回是一个引用,假设有一个struct接收,为什么要返回一个引用的struct而不是直接返回struct呢?
一般的返回值是返回值(临时变量)被复制到一个临时位置,然后再复制给接收的,同理返回结构也是这样,但如果使用返回结构的引用而不是返回结构,那么在函数里新建的struc变量(临时变量)将直接复制给接收的而没有复制到临时位置的这一个过程。
因此下面的代码是错误的,该函数返回了一个指向临时变量的引用,函数运行完它将不再存在。
为了避免这种问题,最简单的就是返回一个参数传递给函数的应用,
另一种方法是用new来分配新的存储函数,也即分配内存后,返回改内存空间的指针。
5)引用变量为程序阅读增加的新难度
注意这里的four是一个结构变量,dup和five都是结构变量,这里首先将five的数据添加到dup中,再用four的内容覆盖dup的内容。
在赋值语句中,左边必须是可修改的左值,也就是左边的子表达式必须标识一个可修改的内存块,这里这个函数反悔了dup的引用,因此它确实标识的是一个这样的内存块。
常规(非引用)返回类型是右值——不能通过地址访问的值,而且由于常规返回值位于临时内存单元中,运行到下一条语句时它们可能不再存在。
6)将应用变量到string上
返回string引用将节省内存
使用const将避免string被修改。
3、临时变量
临时变量上面我们已经介绍过了。
C++中的临时变量_rongwenxiao的专栏-CSDN博客_临时变量
看着相当复杂,编译器在我们看不到的地方做了很多事情,如果X是一个用户自定义的类型,有默认构造函数,拷贝构造函数,赋值运算函数,析构函数,编译器将这些都做了。
这篇文章的大概意思是以传值方式传入函数,由于实参不能修改,这时候需要一个一模一样(除了地址)的临时变量来完成传值的语义,这时候编译器要修改函数的声明,修改在函数里所有调用这个临时变量的地方。
4、函数重载
名称修饰
c++如何追踪每一个重载函数呢?
c++编译器将根据函数原型中指定的形参类型对每个函数名进行加密
去掉修饰变为
5、函数模板
1)模板的使用
int x;
short intervel;
转换为
double x;
short doublerrval;
通过使用函数模板能够自动完成这一过程,省时且可靠
第一种模式
第二种模式
使用的时候,直接传值就行,编译器会自动转化类型。
但是传的类型在一个区域里仅能使用一种类型
#include <iostream>
using namespace std;
template <typename T>
T add(T a, T b)
{
return a + b;
}
int main()
{
int a=2, b = 1;
double c=3.0, d = 2.0;
cout << add(a, d);
}
2)模板重载
T swap(T a,T b);
T swap (T a, T b,int c);
3)模板的局限性
如果T是数组,那么if(a>b)就逻辑出现问题,因为它比较的是数组的地址,
还比如T c=a*b;这种也不成立,也就是如果T是数组、指针或结果,这种假设就不成立;
有两种解决办法
1、重载运算符
2、为特定类型提供具体化的模板定义
这里介绍第二种方法
显式具体化
模板本身并不是函数定义,而传递参数后,编译器为参数的特定类型生成的才是函数定义,也称为模板实例,这种实例化方法称为隐式实例化。
但还有另一种方式,那就是显式实例化。直接写出实例化的代码。比如Swap<int>();这里通过<>指出我传递的类型,并在声明前加上template。
template void Swap <int > (int ,int);
这种编译器看到后将使用Swap()模板生成一个使用int类型的实例。
上面是在函数定义的时候做的工作,此外显示实例化还可以通过使用函数来创建。
cout<<Swap<double>(x,m)<<endl;
即使x和m的类型不是double或者两个类型不同,但是由于实例化了,也可以强制转换为double类型。
还有个名词称为显式具体化,它有两种方式声明
template <> void Swap<int>(int &,int &);
template <> void Swap (int &,int &);
不要在同一文件中使用同一种类型的显式实例和显式具体化,那样会出错
上面提到的这些就是为了重载,可以在一个文件中有多个相同函数名的函数,编译器会确定哪个可行函数是最佳的。它将查看为使函数调用参数与可行的候选函数的参数匹配所需要的进行的转换,
从最佳到最差的顺序如下
- 完全匹配
- 提升转换(将char和shorts自动提升为int,float自动转换为double)
- 标准转换 (将int转换为char,long转换为double)
- 用户定义的转换(类声明中定义的转换)
如果有多个匹配的原型,编译器无法完成重载解析过程。
如果没有最佳的可行函数,编译器会生成诸如二义性这样的错误信息。
如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。
非模板函数优于模板函数。
两个模板都匹配,但有指针的那个已经显式指出函数参数是指向Type的指针,大事第一个模板里Type必须被解释为指向blot的指针,所以第二个指针更加具体。
4)关键字decltype
int x;
decltype(x) y; //将y变为和x一样的类型
decltype(x+y) c; //将c变为和x+y结果后的类型
decltype(x+y) c=x+y; //c为x+y后的类型,并将值赋给它
5)后置返回类型
无法获知返回类型是什么,因此可以通过下面的方法
->decltype(x+y) 是返回类型
auto 函数名(参数)->decltype(变量)
内存模型和名称空间
头文件管理
在同一文件中只能将同一头文件包含一次,可以使用#ifnder来避免
这种方法并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容。
1、存储持续性、作用域和链接性
存储数据方案
- 自动存储持续性:在程序执行时被创建,在执行完函数或代码块时使用的内存被释放
- 静态存储持续性:在函数外定义的变量和使用static定义的变量的存储持续性都是静态,它们在程序整个运行过程中都存在
- 线程存储持续性:程序可以将计算放在可并行处理的不同线程中,如果变量是使用关键字thread_local声明的,则生命周期与所属线程一样长
- 动态存储持续性:用new分配的内存将一直存在,直到使用delete将其释放或者程序结束为止。
名称空间
声明区域
函数在函数外面声明为全局变量,此时声明区域为其声明所在的文件
函数中声明的变量,其声明区域是声明所在的代码块
潜在作用域
潜在作用域从声明点开始到其声明区域的结尾,但是变量并非在其潜在作用域的所有位置都是可见的,比如局部变量会隐藏全局变量
c++新增了通过定义一种新的声明区域来创建命名的名称空间,这样将提供一个声明名称的区域,一个名称空间的名称不会与另外一个名称空间的相同名称发生冲突。
下面是两个名称空间。
名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。
名称空间是开放的,即可以把名称加入到已有的名称空间中。
using namespace elements::fire;
也可以在名称空间中使用using编译指令和using声明。
名称空间指导原则
总结
对象和类
访问控制
定义公有函数去请求私有成员。防止程序直接访问数据被称为数据隐藏。
构造函数
Stock food=Stock("a",1); //显式调用
Stock food1("a",1); //隐式调用
Stock* food1=new Stock("a",1); //使用new动态分配内存
列表初始化
this指针
对象数组
定义
使用
使用类
1、运算符的重载
1)使用
实际上为Box1.operator+(Box2);
是否可以
t4=t1+t2+t3
答案是可以的,但如何转化
t4=t1.operator+(t2+t3); 转化为
我认为是t1与t2运算完,然后返回一个对象,这个对象再和t3进行运算。
当两者类型一样的时候,谁在前面都可以,比如上面的Box1+Box2=Box2+Box1
但是两者类型不一样时,比如3.4*Box 将不等于Box*3.4,这两者的意思完全不一样。
可以使用BOX operator*(double m,const BOX & t);重载
在类中定义重载函数,它有一个隐式的this指针,对于加法只有两个运算符,所以传递的参数应该只有一个,因此在类内只能定义单参数的运算符函数。
如果想要传递两个参数,可以将重载函数定义为友函数,就可以放在外面了
2)重载限制
1、为了防止重载标准类型运算符,因此重载后的运算符必须至少有一个操作数是用户定义的类型。
2、使用运算符不能违反运算符原来的句法规则。
3、运算符的优先级和原来的优先级一样
4、不能创建新运算符。
可以重载的运算符列表
双目算术运算符 | + (加),-(减),*(乘),/(除),% (取模) |
关系运算符 | ==(等于),!= (不等于),< (小于),> (大于),<=(小于等于),>=(大于等于) |
逻辑运算符 | ||(逻辑或),&&(逻辑与),!(逻辑非) |
单目运算符 | + (正),-(负),*(指针),&(取地址) |
自增自减运算符 | ++(自增),--(自减) |
位运算符 | | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移) |
赋值运算符 | =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>= |
空间申请与释放 | new, delete, new[ ] , delete[] |
其他运算符 | ()(函数调用),->(成员访问),,(逗号),[](下标) |
不可重载的运算符
- .:成员访问运算符
- .*, ->*:成员指针访问运算符
- :::域运算符
- sizeof:长度运算符
- ?::条件运算符
- #: 预处理符号
2、友元
上面那个*的例子如果把*的定义放到类的外面那么就无法访问类的私有数据。
但是有一类特殊的成员函数可以访问类的私有成员它们被称为友元函数。
该原型意味着
- 虽然operator*()函数是在类声明中声明的,但它并不是成员函数,不能使用成员运算符来调用,也不能使用Box::限定符在类外定义。
- 虽然operator*()函数不是成员函数,但它与成员函数的访问权限相同。
- 在类外定义函数时不需要加friend
友元是否有悖于OOP?
虽然友元机制允许非成员函数访问私有数据,但是应将友元函数看做类的拓展接口的组成部分,通过使用友元函数和类方法,可以用同一个用户接口表达多种操作,并且只有类声明可以决定哪一个函数是友元,因此类声明仍然控制了哪些函数可以访问私有数据,所以类方法和友元只是表达类接口的两种不同机制
不使用友元用下列方式替代
由于i*Box,要想Box*i,那么就调用这个函数,这个函数会倒换Box和i的位置,调用的是类里面的成员函数,也就没有使用友元。但我编译器没有通过。
重载运算符作为成员函数还是非成员函数
加法运算符需要两个操作数,对于成员函数来说,一个操作数是通过this指针隐式传递,另一个操作数作为函数参数显式的传递,对于友元版本来说,两个操作数都作为参数来传递。
类的自动转换和强制类型转换
- 如果类型兼容,将自动转换。long count=8;
- 如果类型不兼容,可以使用强制转换。 int *p=(int *)10; p指针变量存着10,虽然这个赋值没有意义
- 在使用构造函数时会有隐式转换(可以使用explicit关闭隐式转换)。
在构造函数的转换是在构造函数处理传递来的数据
Complex(double real,double imag) //转换构造函数
{
this->real=real;
this->imag=imag;
} //这种可以把预定义类型转换为自定义类的对象,但是不能把类的对象转换为基本数据类型。比如不能把Complex类的对象转换为double类型
c++采用类型转换函数来解决这个问题
operator 目标类型()
{
...
return 目标类型的数据;
}
我感觉这个比较像对标准类型的重载。
类和动态内存分配
c++如果增加内存负载,如果要创建一个类,其一个成员表示某人的性,如果用一个数组来保存(当然也可以使用string),数组太小的存不下,太大的浪费空间,这种问题的解决办法通常就是在程序运行时(不是编译时)来确定的该使用多少内存。
动态内存和类
c++在分配内存时是让程序在运行时决定内存分配而不是在编译时决定,这样可根据程序的需要而不是根据一系列严格的存储类型规则来使用内存。
不能在类声明中初始化静态成员变量,是因为声明描述了如何分配内存,但并不分配内存,可以使用这个格式来创建对象,从而分配和初始化内存,对于静态类成员,可以在类声明之外使用单独的语句来初始化。
对象中的字符串(char *)成员并不保存在对象中,字符串(char *)单独保存在堆内存中,对象仅保存了指出到哪里去查找字符串(char *)的信息,并没有创建字符串的副本。
删除对象可以释放对象本身占有的内存,但并不能自动释放属于对象成员的指针指向的内存,因此在析构函数中必须使用delete语句释放由new分配的内存。
使用new来分配内存,需要用delete释放内存,使用new[]分配内存,应该使用delete[]释放内存。
特殊成员函数
c++自动提供下列成员函数
- 默认构造函数
- 默认析构函数
- 复制构造函数
- 赋值运算符
- 地址运算符
隐式地址运算符返回调用对象的地址(即this指针的值)
默认构造函数:编译器将提供一个不接受任何参数,也不执行任何操作的构造函数
1)复制构造函数
复制构造函数:复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数)。
StringBad ditto(motto); //calls StringBad(const StringBad &)
StringBad metto=motto; //calls StringBad(const StringBad &)
StringBad also =StringBad(motto);
//calls StringBad(const StringBad &)
StringBad *pStringBad=new StringBad(motto);
//calls StringBad(const StringBad &)
中间两种可能使用复制构造函数直接创建对象,或者生成临时对象在赋给等号左值,最后一种初始化一个匿名对象,并将新对象的地址赋给指针。
无论哪个编译器,当函数按值传递对象或者函数返回对象时,都将使用复制构造函数。
按值传递意味着创造原始变量的一个副本,当编译器生成临时对象时也将使用复制构造函数,例当三个对象相加时编译器可能生成临时的对象来保存中间结果。
上面四种将默认调用复制构造函数。可以看到如果按值传递对象将会调用复制构造函数,如果不想调用去节省调用构造函数的时间以及存储新对象的空间,可以使用引用传递对象。
默认的复制构造函数逐个复制非静态成员(成员复制称为浅复制),复制的成员的值。
在类中声明了一个静态类常量,在复制的过程中将不会变化。
复制构造函数构造后将不会再使用构造函数来构造对象,析构函数会同样执行,有时会出问题,比如静态类常量不会发生变化,本来以为已经发生变化,所以计数会出现异常。
第二类异常是如果有个指针在第一个对象里定义了,第二个对象为第一个对象复制来的,由于指针p的值是一个内存的地址,那么第二个对象的值也是那个地址,当第一个对象使用完析构后将释放那块内存的数据,因此第二个对象的那个值就不能用了。
因此需要有一个显式复制构造函数,类似写构造函数自己去定义函数里面的内容。
深度复制
在复制构造函数里面对于指针的这种类型,可以先申请内存,在使用strcpy这类的函数去复制内容。
赋值运算符
将一个对象赋给另一个对象时,将使用重载的赋值运算符。
初始化对象时,并不一定使用赋值运算符。
比如Book book1("水浒传");
Book book2=book1;
这类可能直接使用复制构造函数,也可能分为两步:先使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值复制给新对象中。因此
初始化总是会调用复制构造函数,而使用=运算符也可能会调用赋值运算符。
赋值运算符的隐式实现也对成员进行逐个复制,同样的静态数据成员不受影响。
C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的,下面是想要改变时的语法
Class_name & Class_name::operator=(const Class_name &);
如果由于默认赋值运算符不合适而导致的异常问题,解决办法就是提供赋值运算符(深度复制)进行定义。
c++11空指针
str=nullptr
使用中括号表示法访问字符
在C++中,两个中括号组成一个运算符–中括号运算符,可以使用operator来重载该运算符。
假设opera是一个String对象,下面是该方法的简单实现:
char &String::operator[](int i)
{
return str[i];
}
指针和对象
当使用定位new去创建一个对象时,对象的地址需要使用[]delete去删除,尤其需要注意的是,当用定位new去创建一个对象时,另一个也在这个内存上创建了对象,需要注意删除的时间,避免删除了另一个不能使用了。
队列
类继承
从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。
其他
类型别名
有两种方法建立别名
- 使用预处理器 #define BYTE char (这样预处理器将在编译时用char替换所有的BYTE)
- 使用c++的关键字typedef来创建别名 typedef char byte
两者区别
1、#define在预处理时进行简单而机械的字符串替换,在编译的时候发现错误。
typedef在编译时处理,有类型检查功能,在自己的作用域里给已经存在的类型一个别名。
2、 typedef可以定义与机器无关的类型,能够起到类型易于记忆的功能也就是可以来定义类型。
define不只是可以为类型取别名,还可以定义常量、变量、编译开关等
3、define没有作用域的限制,typedef有自己的作用域。
指针常量就是指针本身是常量,换句话说,就是指针里面所存储的内容(内存地址)是常量,不能改变。但是,内存地址所对应的内容是可以通过指针改变的。
int * const p //指针常量 指针变量不能修改,这个const修饰的是p,p不能再修改指向的地址
int a,b;
int * const p=&a //指针常量
//那么分为一下两种操作
*p=9;//操作成功 指针指向的变量的值可以修改
p=&b;//操作错误 指针的值不可以修改
常量指针就是指向常量的指针,换句话说,就是指针指向的是常量,它指向的内容不能发生改变,不能通过指针来修改它指向的内容。但是,指针自身不是常量,它自身的值可以改变,从而指向另一个常量。
const int *p = &a; //常量指针
int a,b;
const int *p=&a //常量指针
//那么分为一下两种操作
*p=9;//操作错误 指针指向的变量的值不能修改
p=&b;//操作成功 指针的值可以修改
上面简单记为 指针常量,对指针施加常量,也就是指针的值是常量
常量指针,对常量施加指针,也就是在常量上加的指针,本身人家就是常量
const在int前就是对常量加上枷锁,在指针p前面就是对指针加的枷锁
指针常量:指针是常量
常量指针:常量的指针 指针指向的是常量
#define INTPTR1 int* //INTPTR1是int*的别名
typedef int* INTPTR2; // INTPTR2是int *的别名
int a = 1;
int b = 2;
int c = 3;
const INTPTR1 p1 = &a;
const INTPTR2 p2 = &b;
INTPTR2 const p3 = &c;
INTPTR1 pa,pb; //这里预处理器将声明转换为 int * pa,pb pa是指向int的指针,pb只是 int
但是typedef不会这样,因此typedef好
const INTPTR1 p1是一个常量指针,即不可以通过p1去修改p1指向的内容,但是p1可以指向其他内容。
const INTPTR2 p2是一个指针常量,不可使p2再指向其他内容。因为INTPTR2表示一个指针类型,因此用const限定,表示封锁了这个指针类型。
INTPTR2 const p3是一个指针常量
迷惑
cin连续输入
抽取运算符>>被设计的使的cin>>a也是一个istream对象,然后将抽取运算符用于y,这样也将获得一个istream对象,因此cin>>a>>b最终表达式是cin,而cin被用于测试表达式时将被转换为bool值。
如果正常写的话
这时候输入一个用于终止循环的值
可以看到连续输入能够简化代码,如果想清空上次遗留的输入,可以通过cin.clear()
unordered_map
#include < unordered_map >
优点: 因为内部实现了哈希表,因此其查找速度非常的快
缺点: 哈希表的建立比较耗费时间
适用处:对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map
unordered_map<int, string> myMap={{ 5, "张大" },{ 6, "李五" }};//使用{}赋值
unordered_map<int, string> myMap={{ 5, "张大" },{ 6, "李五" }};//使用{}赋值
myMap[2] = "李四"; //使用[ ]进行单个插入,若已存在键值2,则赋值修改,若无则插入。
myMap.insert(pair<int, string>(3, "陈二"));//使用insert和pair插入
//遍历输出+迭代器的使用
auto iter = myMap.begin();//auto自动识别为迭代器类型unordered_map<int,string>::iterator
while (iter!= myMap.end())
{
cout << iter->first << "," << iter->second << endl;
++iter;
}
//查找元素并输出+迭代器的使用
auto iterator = myMap.find(2);//find()返回一个指向2的迭代器
if (iterator != myMap.end())
cout << endl<< iterator->first << "," << iterator->second << endl;
system("pause");
静态类成员函数
可以将成员函数声明为静态的(函数声明必须包含关键字static),这样就有两个结果:
首先,不能通过对象调用静态成员函数;如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。例如,可以给String类添加一个名为HowMany()的静态成员函数,方法是在类声明中添加如下原型/定义:
statics int HowMany(){return num_strings;}
调用它的方式如下:
int count=String::HowMany(); //invoking a static member function
由于静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。
返回对象的三种情况梳理
返回指向const对象的引用
下面两种都是可以的。
返回对象会调用复制构造函数,返回引用不会。
返回指向非const对象的引用
返回对象
将会调用复制构造函数去创建返回的对象。
返回const对象
为了避免输错而使的下面第一条语句成立(由于+运算使的返回一个对象,这个对象又赋值后面的)
可以通过加上返回const
注意
1、不要返回指向局部变量或临时对象的引用,函数执行完毕后,局部变量和临时对象将消失,引用将指向不存在的数据。