文章目录
- C++特点:
- 从一个小程序开始
- 输出输入小结
- 函数的重载overloading
- 复杂的数据类型
- 模板:(泛型程序设置 )
- 结构(对象的基础)杂交的产物
- 传值、传址和传引用
- 联合、枚举和类型别名
- 对象
- 定义构造器(他是类里的特殊方法)
- 定义析构器
- This 指针
- 类的继承
- 继承机制中的构造器和析构器
- 注意问题
- 访问控制
- 覆盖(overriding)方法
- 重载方法
- 友元关系
- 静态属性跟静态方法
- 静态方法(联合this指针)
- 虚方法 virsual method
- 抽象方法
- 多态性
- 运算符重载
- 重载<<操作符
- 多继承
- 虚继承
- 错误处理及调试
- 让函数返回错误代码
- assert函数和捕捉异常
- 用户体验与程序员体验
- 捕获异常
- 动态内存管理
- 动态数组
- 从函数或方法返回内存
- 为什么不应该让函数返回一个指向局部变量的指针
- 副本构造器
- 静态对象强制类型转换
- 动态对象强制类型转换
- 避免内存泄漏
- 命名空间和模块化编程
- 命名空间
- 链接和作用域
- 基本的模板语法
- 内联函数
- 容器和算法
- 向量容器
- 迭代器
- 算法
C++特点:
精练小巧简单、面向对象的。任何事物都可以看做为一个对象。
一个在复杂的结构都是由千千万万个对象组成。
对象两个要素:属性跟行为。
面对对象是一种思维。
每个对象都是一个完整的独立的个体,它是由相关属性跟行为组合,与外界分隔。面向对象是当前软件开发方法的主流。GNOME是c语言开发的,模拟的面向对象。
封装
封装意味着把对象的属性跟方法结合成一个独立的系统单位,但尽可能地隐藏对象的内部细节。封装是面向对象的基础,从此程序员面对的就不再是许多复杂的函数和过程实现,而是少数具有行为能力的个体实例
抽象
抽象的过程是对具体问题进行概括的过程,是对一类公共问题进行统一描述的过程。类似于接口用于交流。
继承
子类对象拥有与其基类(父类)相同的全部属性和方法,称为继承。
多态
指在基类中定义的属性和行为被子类继承之后,可以具有不同数据类型或者表现行为等特性。
从一个小程序开始
问题:对一个整形数组求和?
要求:定义一个储存n个元素的数组,用c语言完成
#include<stdio.h>
int addArray(int array[],int n);
int main()
{
int data[]={0,1,2,3,4,5,6,7,8,9};
int size=sizeof(data)/sizeof(data[0]);
//printf("data : %d\n",sizeof(data));
printf("结果是:%d\n", addarray(data,size));
return 0;
}
int addArray(int array[],int n)
{
int sum=0;
int i;
//printf("array : %d\n",sizeof(array));chuan di de shi zhi zhen
for(i=0;i<n;i++)
{
sum+=array[i];
}
return sum;
}
c的数组跟指针很复杂.
#include<iostream>//定义了ostream类 c92标准带.h作为头文件 c99标准不带.h作为头文件,引用了using namespace std;
using namespace std;//命名空间 就是C++标准库所使用的所有标识符(即类、函数、对象等名称)都是在同一个特殊的名字空间(std)中来定义的,如果不使用这条指令,可以通过std::cout来调用输出流。
int addArray( int *array,int n);
int main()//主函数
{
int data[]={0,1,2,3,4,5,6,7,8,9};
int size=sizeof(data)/sizeof(data[0]);
cout<<"jieguoshi:"<<addArray(data,size)<<endl; //cout就是一个输出流对象console out 控制台输出简写,流向cout对象。 “<<”在c语言是左移,但是在C++还是一样,这就是C++的重载。重载事实上就是允许我们按照不同的方式使用同一个操作符。
return 0;
}
int addArray(int *array, int n) //调用函数
{
int sum=0;
int i;
for(i=0;i<n;i++)
{
sum+=*array++;
}
return sum;
}
第二个小程序(IO)
要求:编写一个程序,要求用户输入一串整数和任意数目的空格,这些整数必须位于同一行中,但允许出现在该行的任意位置。当用户按下“enter”键时,数据输入结束。程序自动对所有整数进行求和并打印出来。
#include<stdio.h>
#include<stdlib.h>
void main()
{
int sum=0;
int i;
char ch;
printf("请输入一串整数和任意数目的空格:");
while(scanf("%d",&i)==)
{
sum+=i;
while((ch=getchar())==' ')//ASIC的空格 作用屏蔽空格
; //无内容
if(ch=='\n') //如果接受到回车,退出循环
{
break;
}
ungetc(ch,stdin); //没有获得字符,把变量ch中存放的字符退回到stdin输入流
}
printf("jieguo: %d ",sum);
printf("\n");
system("pause");
}
#include<iostream>
using namespace std;
int main() //任意位置声明变量,提高了可读性 而c语言只能在开头位置
{
int sum=0;
cout<<"请输入一串整数跟任意空格:";
int i;
while(cin>>i) //使用了流对象cin,这个对象的类型是istream,他知道如何从终端读取数据。cin输入操作符也称提取操作符,一次提取一个整数。当用户进行键盘输入时,对应的字符输入到操作系统的键盘缓冲区。当用户点击键盘上的“enter”时,操作系统的键盘缓冲区把内容传到cin流的内部缓冲区(stdin),>>操作符随后从这个内部缓冲区提取信息。
{
sum+=i;
while(cin.peek()==' ') //cin是输入流的一个对象,“.”代表结构 peek称作为行为(也叫函数、方法)作用是获取输入流里第一个字符,但是第一个字符仍然在输入流里
{
cin.get();
}
if(cin.peek()=='\n')
{
break;
}
}
cout<<"结果是:"<<sum<<endl;
return 0;
}
cin跟cout对象的多个方法
样例1:cin.ignore()&cin.getline
#include<iostream>
using namespace std;
int main()
{
char buf[20];
cin.ignore(7);//忽略了前7个字符
cin.getline(buf,10);//获得一句话从buf的第10个字符
cout<<buf<<endl; //endl作用回车加清空缓存区
}
样例2:cin.get()&cin.peek()
#include<iostream>
using namespace std;
int main()
{
char p;
cout<<"请输入一段文本:\n";
while(cin.peek()!='\n')//从字符里挑取不为回车的字符
{
cout<<(p=cin.get());//获取字符
}
cout<<endl;
retirn 0;
}
样例3:cin.gcount()&cin.read()
#include<iostream>
using namespace std;
int main()
{
const int SIZE=50;//const 跟宏定义差不多#define SIZE 50
char buf[size];
cout<<"请输入一段文本:\n";
cin.read(buf,20);//读取20个字符到缓冲区
cout<<"字符串收集到字符的数量:"
<<cin.gcount<<endl;//计算提取了多少个字符
cout<<"输出文本信息为:";//从缓冲区输出20个字符
cout.write(buf,20);
cout<<endl;
retirn 0;
}
cout.precision(): //输出精度
cout.width(): //输出宽度
第三个小程序(File)
要求:编写一个文件复制程序,功能实现将一个文件复制到另一个文件。
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char*argv[]) //整型变量argc字符指针数组argv[],argc的含义是程序的参数数量・包含本身。argv[]的每个指针指向命含行的一个字符串・所以argv[0]指向字符串" copyfile. exe"argv[1]指向字符串source file。argv[2]指向字符串destfile.
{
FILE*in,*out; // in和out是我们声明的两个文件指针,它们的类型都是FILE*,分别作为两个I/O流对象使用
int ch; //getc()的返回值是int类型,所以我们声明时应该是 int ch 而不是 char ch
if(argc!=3) //if(argc3)是为了确保程序参数个数的正确性
{
fprintf(stderr,"输出形式:copy file源文件名 目标文件名 \n");
exit(EXIT_FAILURE);
}
if((in=fopen(argv[1],"rb"))==NULL) //通过 fopen()函数我以二进制的形式按可读/可写方式打开两个文件并返回两个文指针给in和out
{
fprintf(stderr,"打不开文件:%s \n",argv[1]);//为了确保文成功打开・我们对fopen()的返回值进行了检查,如果未成功打开,我就向标准错误流stderr发送一条消息
exit(EXIT_FAILURE);
}
if((out=fopen(argv[2],"wb"))==NULL)
{
fprintf(stderr,"打不开文件:%s \n",argv[2]);
fclose(in);
exit(EXIT_FAILURE);
}
while((ch=getc(in))!=EOF)//getc()函数一次从输入流(stdin)读取一个字符。当 getc()遇到文件结束标志的时候,函数就返回EOF,EOF是一个宏,在stdio.h中定义,其值为一个负整数 ,通常是-1
{
if(putc(ch,out)==EOF)//putc()函数把这个字符写入到输出流(stdout)
{
break;
}
}
if(ferror(in))
{
printf("读取文件 %s 失败 \n",argv[1]);
}
if(ferror(out))
{
printf("写入文件 %s 失败 \n",argv[2]);
}
printf("成功复制一个文件\n");
fclose(in);
fclose(out);
return 0;
}
#include<fstream>
#include<iostream>
using namespace std;
int main()
{
ifstream in; //ifstream是一个文件读取类 in是对象
in.open("test.txt"); //open函数打开txt文本
if(!in)
{
cerr<<"打开文件失败"<<endl;
return 0;
}
char x;
while(in>>x)
{
cout<<x;
}
cout<<endl;
in.close();
return 0;
}
#include<fstream>
#include<iostream>
using namespace std;
int main()
{
ofstream out; //ofstream是一个文件写入类 out是对象
out.open("test.txt"); //open函数打开txt文本
if(!out)
{
cerr<<"打开文件失败"<<endl;
return 0;
}
for(int i=0;i<10;i++)
{
out<<i;
}
out<<endl;//
out.close();
return 0;
}
#include<fstream>
#include<iostream>
using namespace std;
int main()
{
ofstream out("test.txt",ios::app); //ofstream是一个文件写入类 out是对象,默认的使用open函数(方法) ios::app是一种文件操作,作用是写入的所有数据被追加到文件的末尾
if(!out)
{
cerr<<"打开文件失败"<<endl;
return 0;
}
for(int i=10;i>0;i--)
{
out<<i;
}
out<<endl;//
out.close();
return 0;
}
多种文件操作用or操作符"|"
#include<fstream>
#include<iostream>
using namespace std;
int main()
{
fstream fp("test.txt",ios::in|ios::out); //fstream是一个文件类 fp是对象,默认的使用open函数(方法)
if(!fp)
{
cerr<<"打开文件失败"<<endl;
return 0;
}
fp<<"i love fish";
static char str[10];//静态变量,不被改变
fp.seekg(ios::beg);//文件指针指向文件头 iOS::end则是文件尾
fp>>str;
cout<<str<<endl;
fp.close();
return 0;
}
输出输入小结
题目:程序向用户提出一个”y/n“问题,然后把用户输入的值赋值给answer变量
要求:针对用户输入的”Y/y“和”N/n“进行过滤
#include<iostream>
int main()
{
char answer;
std::cout<<"请问可以删除你的视频吗??【y/n】";
std::cin>>answer;
switch(answer)
{
case 'Y':
case 'y':
std::cout<<"请不要随便删除视频";
break;
case 'N':
case 'n':
std::cout<<"干的漂亮";
break;
default :
std::cout<<"你的输入不符合要求";
break;
}
std::cin.ignore(100,'\n'); //直到遇到回车结束程序
std::cout<<"输入任何字符结束"<<"\n";
std::cin.get(); //相当与pause
return 0;
}
题目:编写一个温度转换程序,提示用户以【xx.x C】或【xx.x F】的格式输入。
要求:如果用户输入34.2 C 程序自动转换为90.32 F并输出。
#include<iostream>
int main()
{
//华氏温度==摄氏温度*9.0/5.0+32
const unsigned short ADD_SUBTRACT=32;//静态变量类似于宏定义
const double RATIO=9.0/5.0;
double tempIn,tempOut;
char typeIn,typeOut;
std::cout<<"请按照格式【xx.x C】或【xx.x F】输入温度:";
std::cin>>tempIn>>typeIn;
std::cin.ignore(100,'\n');//忽略回车
switch(typeIn)
{
case 'C':
case 'c':
tempOut=tempIn*RADIO+ADD_SUBTRACT;
typeOut='F';//输出大写
typeIn='C';//强制输入大写
break;
case 'F':
case 'f':
tempOut=(tempIn-ADD_SUBTRACT)/RADIO;
typeOut='C';
typeIn='F';
break;
default:
typeOut='err';
break;
}
if(typeOut!='err')
{
std::cout<<tempIn<<typeIn
<<"="<<tempOut<<typeOut<<"\n\n";//语句不分行,一个语句以分号为准。
}
else
{
std::cout<<"输入错误 \n\n";
}
std::cout<<"输入任何字符结束"<<"\n\n";
std::cin.get(); //相当与pause
return 0;
}
对输入数据进行合法性检测:符合实际
函数的重载overloading
函数重载:实质就是用同样的名字在定义有着不同参数但有着同样用途的函数。(类似于多重人格)注意:可以是参数个数上的不同,也可以是参数数据类型的不同。
实例:
#include<iostream>
void convertTemperature(double tempIn,char typeIn);//函数声明
void convertTemperature(int tempInInt,char typeIn);//函数重载
int main()
{
double tempIn;
int tempInInt;
char typeIn;
std::cout<<"请按照格式【xx.x C】或【xx.x F】输入温度:";
std::cin>>tempIn>>typeIn;
std::cin.ignore(100,'\n');//忽略回车
std::cout<<"\n";
convertTemperature(tempIn,typeIn);//函数调用
std::cout<<"请按照格式【xx C】或【xx F】输入温度:";
std::cin>>tempInInt>>typeIn;
std::cin.ignore(100,'\n');//忽略回车
std::cout<<"\n";
convertTemperature(tempInInt,typeIn);//函数调用
return 0;
}
void convertTemperature(double tempIn,char typeIn)//函数定义
{
const unsigned short ADD_SUBTRACT=32;//静态变量类似于宏定义
const double RATIO=9.0/5.0;
double tempOut;
char tempOut;
switch(typeIn)
{
case 'C':
case 'c':
tempOut=tempIn*RADIO+ADD_SUBTRACT;
typeOut='F';//输出大写
typeIn='C';//强制输入大写
break;
case 'F':
case 'f':
tempOut=(tempIn-ADD_SUBTRACT)/RADIO;
typeOut='C';
typeIn='F';
break;
default:
typeOut='err';
break;
}
if(typeOut!='err')
{
std::cout<<tempIn<<typeIn
<<"="<<tempOut<<typeOut<<"\n\n";//语句不分行,一个语句以分号为准。
}
else
{
std::cout<<"输入错误 \n\n";
}
std::cout<<"输入任何字符结束"<<"\n\n";
std::cin.get(); //相当与pause
}
void convertTemperature(int tempIn,char typeIn)//函数声明
{
const unsigned short ADD_SUBTRACT=32;//静态变量类似于宏定义
const double RATIO=9.0/5.0;
int tempOut;
char tempOut;
switch(typeIn)
{
case 'C':
case 'c':
tempOut=tempIn*RADIO+ADD_SUBTRACT;
typeOut='F';//输出大写
typeIn='C';//强制输入大写
break;
case 'F':
case 'f':
tempOut=(tempIn-ADD_SUBTRACT)/RADIO;
typeOut='C';
typeIn='F';
break;
default:
typeOut='err';
break;
}
if(typeOut!='err')
{
std::cout<<tempIn<<typeIn
<<"="<<tempOut<<typeOut<<"\n\n";//语句不分行,一个语句以分号为准。
}
else
{
std::cout<<"输入错误 \n\n";
}
std::cout<<"输入任何字符结束"<<"\n\n";
std::cin.get(); //相当与pause
}
复杂的数据类型
设计思路及使用习惯 复杂=简单+简单
主要讲三种数据类型:数组、指针(一种更加重要跟抽象的数据类型)、结构
数组
优点:一个数组可以把许多同类型的值储存在同一个变量名之下。
定义:
type name[x]; //type 是指是int、float、double、char等
实例:定义一个数组容纳十个整数,这些整数均来自用户输入。我们计算这些值得累加和、平均值输出。
#include<iostream>
using namespace std;
int main()
{
int num[10];
cout<<"请输入10个整形数据:";
for(int i=0;i<10;i++)
{
cout<<"请输入第"<<i+1<<"个数据:";
cin>>num[i];
}
int total=0;//局部变量存放在栈里,不能初始化。此处进行初始化
for(int j=0;j<10;j++)
{
total+=num[j];
}
cout<<"总和是:"<<total;
cout<<"平均值是:"<<(float)total/10; //(float)强制转换为浮点型
return 0;
}
优化可以用宏变量测N个值的和与平均值:
#define item 5 测5个值的和与平均值:
或const unsigned short item 5 测5个值的和与平均值:
输出打印
#include<iostream>
#include<string>
using namespace std;
int main()
{
string str;
cout<<"请随便输入字符串:";
getline(cin,str);//获得一句话从cin的str字符串
//cin>>str;//不用getline遇见空格就结束
cout<<str;
return 0;
}
指针(神话大佬)
完成其他办法无法完成的任务,想了解指针,首先了解地址。
int a=-12; //整型占4个字节
char b=M;//字符型占1个字节
float c=pi;//浮点型占4个字节
在C++里,变量类型根据他们的自然边界进行对齐!!!知道即可,编译器会帮我们自动处理这类问题。
32位对应4个字节,所以自动对齐4个开始。 64位对应8个字节,所以自动对齐每8个开始。
地址从0开始,每一个地址指向一个字节
寻址:
两种方法索引
1.通过变量名
2.通过地址
取值操作符 & 作用是获取变量的地址
扩展知识:
1.对齐:在计算机底层随处可见
内存对齐
文件对齐
32位系统内存对齐值是:1000H=4kb //H是页
64位系统内存对齐值是:2000H=8kb
文件对齐值是:200H // 存放不需要处理,所以不需要太大的内存值,文件读取到内存时会转换为1000H
2.变量地址
变量的地址在程序执行期间是不会发生变化的。
不过同一个程序不同时间加载到内存中,同一个变量的地址是会改变的。(这里涉及WINDOWS加载器原理及系统对程序的欺骗)
指针
定义:指针是专门用来存放地址的特殊变量类型。
声明:type *pointerName;
如
int *p;
int pp=123;
p=&pp; //pp变量的地址给了p
注意:指针变量前边的类型是用来说明指针指向的数据类型,请务必匹配来使用。
另外允许void类型的指针变量:void *p;
利用指针改变值
int a=456;
char b='c';//ASCII码为‘67’
int *apointer=&a;
char *bpointer=&b;
//让程序保留4个内存块,两个为变量保存,两个为指针保留。变量a、b存放的是变量的值。两个指针变量存放着指针的值,这些值是其他变量的地址。
四个内存块是因为对其原因。
当我们知道了某个变量在内存中的地址(通过指针),就可以利用指针访问位于该地址的数据。这需要对指针进行”解引用deference“处理:即在指针名前加上一个星号(*)。
如:std::cout<<*apointer;
//解释一下,把整数变量a的地址存储在apointer指针里面后,*apointer和变量a将代表同一个值。
实例:
#include<iostream>
using namespace std;
int main()
{
int a=123;
float b=3.14;
char c='c';
unsigned long d = 19880808;
//声明5个变量
string e='i love you';
cout<<"a的值是:"<<a<<"\n";
cout<<"b的值是:"<<b<<"\n";
cout<<"c的值是:"<<c<<"\n";
cout<<"d的值是:"<<d<<"\n";
cout<<"e的值是:"<<e<<"\n";
//输出5个变量
int *apointer=&a;
float *bpointer=&b;
char *cpointer=&c;
unsigned long dpointer=&d;
string *epointer=&e;
//声明5个指针
*apointer=456;
*bpointer=4.13;
*cpointer='f';
*dpointer=20111124;
*epointer="i love beauty";
//解引用五个指针,另赋值
cout<<"a的值是:"<<a<<"\n";
cout<<"b的值是:"<<b<<"\n";
cout<<"c的值是:"<<c<<"\n";
cout<<"d的值是:"<<d<<"\n";
cout<<"e的值是:"<<e<<"\n";
//打印出来
}
课后总结:指针所保存的是内存中的一个地址。它并不保存指向数据的值的本身。因此,务必确保指针对应一个已经存在的变量或者已经分配的内存。
星号的两种用途:
1.用于创建指针 :int *apointer=&a;
2.用于对指针进行解引用:*apointer=456;
c++允许指针群P,就是多个指针有同样的值:
int *p1=&a
int *p2=&a
c++支持无类型指针(void),就是没有被声明为某种特定类型的指针.
列如:void *vpointer; //对一个无类型指针进行引用前,必须先把它转换为一种适当的数据类型。
指针和数组
标量类型:整数、实数和字符。
#include<iostream>
using namespace std;
int main()
{
const unsigned short ITEM =5;
int intarray[ITEM]={1,2,3,4,5};
char chararray[ITEM]={'B','I','G','E','R'};
//定义两个数组
int *intptr = intarray;
char *charptr = chararray;
//定义两个指针指向上面两个数组
cout<<"整型数组输出:"<<'\n';
for(int i=0;i<ITEM;i++)
{
cout<<"*intptr"<<"at"<<reinterpret_cast<unsigned long>(intptr)<<'\n'; //强制类型转换
intptr++;
}
cout<<"字符型数组输出:"<<'\n';
for(int i=0;i<ITEM;i++)
{
cout<<"*charptr"<<"at"<<reinterpret_cast<unsigned long>(charptr)<<'\n';
charptr++;
}
return 0;
}
模板:(泛型程序设置 )
数组的名字同时也是指向第一个元素(基地址)的指针
结构(对象的基础)杂交的产物
结构structure:是一种由程序员定义的、由其他变量类型组合而成的数据类型。
定义一个结构的基本语法:
struct name
{
type varname1;
type varname2;
...
}; //注意结尾分号,变量(成员)个数没有限制
定义了一个结构之后,就可以使用如下所示的语法来创建该类型的变量。
fishoil jiayu; //创建一个fishoil结构类型的jiayu
jiayu.name="xiaojiayu";
jiayu.uid="fishc_0001";
jiayu.sex='M'; //用"."对结构成员进行赋值
或者
fishoil jiayu ={"xiaojiayu","fishc_0001",'M'};
结构与指针
指针无所不能,也可以指向结构,但是怎么指向结构内成员?
fishoil *pjiayu=&jiayu;//创建一个结构指针
注意:指针的类型必须跟指向地址的变量的类型一致。
例子:
要求定义一个结构,至少储存:姓名、身份证、性别,实现文件存储,打印在屏幕上。
#include<iostream>
#include<fstream>
#include<iostream>
传值、传址和传引用
传值
#include<iostream>
void changeage(int age, int newage);
main()
{
int age =24; //age 是实参
std::cout<<"my age is :"<<age<<"\n";// 24
changeage(age,age+1);
std::cout<<"now my age is :"<<age<<"\n";//24
}
void changeage(int age, int newage)
{
age=newage; //*age=newage; 注释的是传址
std::cout<<"in this,my age is :"<<age<<"\n"; //25
}
传址
#include<iostream>
void changeage(int *age, int newage);
main()
{
int age =24; //age 是实参
std::cout<<"my age is :"<<age<<"\n";// 24
changeage(&age,age+1);
std::cout<<"now my age is :"<<age<<"\n";//25
}
void changeage(int *age, int newage)
{
*age=newage;
std::cout<<"in this,my age is :"<<*age<<"\n"; //25
}
传递引用
传递引用
#include<iostream>
using namespace std;
void swap(int *x;int *y);
main()
{
int x,y;
cout<<"请输入两个不同的值:";
cin>>x>>y;
swap(&x,&y);
cout<<"调用后输出:"<<x<<' '<<y<<"\n";
}
void swap(int *x;int *y)
{
int temp;
temp=*x;
*x=*y;
*y=temp;
//*x^=*y; //x异或变成中间值,中间值异或两个值
//*y^=*x;
//*x^=*y; // 用异或代替temp
}
联合、枚举和类型别名
到目前为止,我们已经见过整数、实数、字符、字符串、数组、指针和结构。今后还会讨论一种由于数组的向量(vector)类型。
介绍几个非主流的类型:联合、枚举、类型别名。
联合:(union)
联合与结构有很多类似,联合也可以容纳多种不同的数据类型,但是它每次只能储存这些值中的某一个。
样例:
union mima
{
unsigned long birthday;
unsigned short ssn;
char * pet;
};
定义了这个联合类型后,就可以创建该类型的变量:mima mima_1;
在接下来,我们可以像对结构成员进行赋值那样对联合里的成员赋值,使用的语法: mima_1.birthday=19981212;
上面的这条语句是将值19981212存入mima_1联合的birthday里边。如果我们再执行下边语句:mima_1.pet="chaozai";
这个联合将把“chaozai”存入mima_1联合的pet成员,并丢弃birthday成员里的值。
枚举:(enum)
用来创建一个可取值列表:
enum weekdays{monday,tuesday,wednesday,thursady,friday}; //注意,这里不需要使用引号,因为枚举值不是字符串。
编译器会按照枚举值在定义时出现的先后顺序把他们与0——n-1的整数分别关联起来。(n是枚举值总个数)
定义一个枚举类型后,我们就可以像下面这样创建该类型的变量; weekdays today;
然后我们像下面这样给它赋值:today thursday;
使用枚举值的好处:它们对变量的可取值加以限制
它们可以用做switch条件语句的case标号(因为字符串是不能作为标号用的!!!小技巧)
样例:
#include<iostream>
int main()
{
enum weekdays{monday,tuesday,wednesday,thuesday,friday};
weekdays today;
today=monday;
std::cout<<today<<"\n"; //0
today=tuesday;
std::cout<<today<<"\n";//1
switch (today)
{
case monday:
break;
}
}
类型别名
类型别名(Typedef)保留字,使用它可以为一个类型定义创建一个别名。
例如,我们不喜欢使用int*来创建指针,可以像下边这样定义一个类型别名:typedef int* intpointer;
在此之后,我们就可以像下面定义整型指针:intpointer mypointer;
对象
使用对象是C++的核心。对象的本质只不过是一种新的数据类型。
1.我们从一个类开始,首先类是一个模型。当我们为这个类创建实例的时候,也就是对象本身。
2.这跟我们之前讲解的定义和使用结构的过程很相似,但这更具有扩展性和前瞻性。
3.对于初学者来说,我们先给大家一个区别:对象的内部可以有变量跟函数,而结构通常只有各种变量构成。
创建简单的类:
我们首先需要知道的一件事是如何编写一个简单对象的模型----类。
类class就像是一副蓝图,他决定对象是什么样的(具备什么属性、功能)
所以OOP过程的第一步是创建一个类,而每个类跟变量一样都有一个名字,我们就从如何声明一个类说起。
class myfirstclass
{
};
注意,类名的第一个字母采用大写字母是一种习惯,但不是硬性规定。类在声明末尾必须要有一个分号,这一点跟C++结构体情况相同。
类由变量跟函数构成,对象将使用那些变量来存储信息,调用那些函数来完成操作。
所以人们常常会看到一些专门术语:类里边的变量称为属性,函数称为方法。注意本质没有改变。
样例:
class Car
{
//属性
public://关键词
std::string color;
std::string engine;
float gas_tank;
unsigned int Wheel;
//定义方法:先在类的声明里创建一个方法的原型,稍后实现这个方法。
void fill_tank(float liter);//方法的声明:方法是加油,参数是liter
}
具体例子:
#include<iostream>
class Car
{
public://关键词
std::string color;
std::string engine;
float gas_tank;
unsigned int Wheel;
void fill_tank(float liter);//现在我们的Car类有了一个名为fill_tank的方法,他只有一个输入参数,不需要任何返回值。但是我们只有他的原型(声明),想使用他,我们还要对这个函数进行正式的定义(即告诉系统如何让去实现它)。方法的定义通常安排在类声明的后面。
void running();
};
void Car::fill_tank(float liter)//作用域解析操作符(::)作用是告诉编译器这个方法在何处,或者说属于哪一个类。
{
gas_tank += liter;
}
int main()
{
Car mycar;
mycar.fill_tank(4);
return 0;
};
有些程序员喜欢把类的声明和类的函数的定义分别存入两个不同的文件,前者存入.h文件,后车存入相应的.cpp文件。
C++允许在类里声明常量,但不允许对于他进行赋值,例如:
class Car
{
//属性
public://关键词
const float TANKSIZE = 85;//出错
} //绕来这一个限制的方法是创建一个静态常量:
class Car
{
//属性
public://关键词
const float TANKSIZE = 85;//出错
static const float TANKSIZE = 85; //可以
}
还有其他的方法。
类可以在声明某个类的同时创建一些该类的对象:
class Car
{
//属性
public://关键词
....
} car1,car2;// 但不建议这么使用。
定义构造器(他是类里的特殊方法)
构造器跟通常方法的主要区别:
1.构造器的名字必须和他所在类的名字一样
2.系统在创建某一个类的实例时会第一时间自动调用这个类的构造器
3.构造器永远不会返回任何值。
常见构造器,需要先把声明添加到类里:
class car
{
car(void);
}
定义析构器
一般来说,构造器用来完成事先初始化和准备工作(申请分配内存),析构器用来完成事后所必需的清理工作(清理内存)。
构造器跟析构器二者相辅相成,有许多共同之处。首先析构器有着和构造器/类一样的名字,只不过前面多一个波浪符“~”前缀。
class car
{
car(void);
~car();
}
其次,析构器也永远不返回任何值。
另外,析构器是不带参数的,所以析构器的声明永远是下面的格式:~classname();
样例:
#include<iostream>
#include<string>
#include<fstream>
class storequote
{
public:
std::string quote,speaker;
std::ofstream fileoutput;
storequote();
~storequote();
void inputquote();
void inputspeaker();
bool write();
};
storequote::storequote()
{
fileoutput.open("text.txt",std::ios::app);//打开一个文件 构造器
}
storequote::~storequote()
{
fileoutput.close();
}//文件关闭 析构器
void storequote::inputquote()
{
std::getline(std::cin,quote);
}//写入名言
void storequote::inputspeaker()
{
std::getline(std::cin,speaker);
}//写入名言作者
bool storequote::write()
{
if(fileoutput.is_open())
{
fileoutput<<quote<<"|"<<speaker<<"\n";
return true;
}
else
{
return false;
}
}
int main()
{
storequote quote;
std::cout<<"请输入一句名言:";
quote.inputquote();
std::cout<<"请输入一句名言作者:";
quote.inputspeaker();
if(quote.write())
{
std::cout<<"文件写入成功 ";
}
else
{
std::cout<<"文件写入失败 ";
return 1;
}
return 0;
}
This 指针
this指针是类自动生成、自动隐藏的私有成员,它存在于类的非静态成员函数中,指向被调用函数所在的对象的地址。当一个对象被创建时,该对象的this指针就自动指向这个对象的首地址。
this->fishc=fishc;
这样编译器就懂了,赋值操作符的左边将被解释为当前对象fishc的属性,右边将被解释为构造器的传入来的fishc参数。
注意:
使用this指针的基本原则:如果代码不存在二义性隐患,就不必使用this指针。this指针在一些更高级的方法中也会用到。
类的继承
继承是面对对象编程技术的一个核心概念。
继承机制使得程序员可以创建一个类的堆叠层次结构,每个子类均将继承在他基类里定义的方法跟属性。
语法: class Subclass(子类):public Superclass(基类、父类、超类){…}
#include<iostream>
#include<string>
class Animal
{
public:
std::string mouth;
void eat();
void sleep();
void drool();
};//基类:共有的方法
class Pig:public Animal
{
public:
void climb();
};//子类
class Turtle:public Animal
{
public:
void swim();
};//子类
void Animal::eat()
{
std::cout<<"i am eating "<<std::endl;
}
void Animal::sleep()
{
std::cout<<"i am sleeping "<<std::endl;
}
void Animal::drool()
{
std::cout<<"i am man "<<std::endl;
}
void Pig::climb()
{
std::cout<<"i am climbing "<<std::endl;
}
void Turtle::swim()
{
std::cout<<"i am swimming "<<std::endl;
}
int main()
{
Pig pig;
Turtle turtle;
pig.eat();
turtle.eat();
pig.climb();
turtle.swim();
return 0;
}
继承机制中的构造器和析构器
在没有继承机制的情况下,我们很容易理解这些方法在创建或销毁一个对象的时候被调用。但是一旦使用了继承机制,构造器跟析构器就变得有点复杂啦。
比如说基类有一个构造器,如AnimalI(),它将在创造Pig类型的对象时最先被调用。如果Pig类也有一个构造器,它将排在第二个被调用。因为基类必须在子类之前初始化原则。
然后我们继续讨论:如果构造器带有参数,事情将会变得复杂。
class Animal
{
public:
Animal(std::string thename); //构造器 有一个字符串类型的参数thename
std::string name;
}
class Pig:public Animal
{
public:
Pig(std::string thename);//构造器
}
方法如何定义呢??
Animal::Animal(std::string thename)
{
name=thename;
}
Pig::Pig(std::string thename):Animal(thename) //注意在子类构造器里定义的“:Animal(thename)”的语法含义:当调用Pig()构造器时(以thename作为输入参数),Animal()构造器也将被调用(thename输入参数传递给它)。于是我们调用Pig pig("小猪猪");将把字符串“小猪猪”传递给Pig()和Animal(),赋值动作实际发生在Animal()方法里。
{}
在销毁某个对象时,基类的析构器也将自动调用,但这些事情编译器会帮你自动处理。
因为析构器不需要输入参数,所以根本用不到使用SuperClassMethod(arguments)语法!!
与构造器相反,基类的析构器在子类的最后一条语句执行完毕后才被调用。
#include<iostream>
#include<string>
class BaseClass
{
public:
BaseClass();
~BaseClass();
void doSomething();
};
class Subclass : public BaseClass
{
public:
Subclass();
~Subclass();
};
BaseClass::BaseClass()
{
std::cout<<"我进入了基类构造器";
}
BaseClass::~BaseClass()
{
std::cout<<"我进入了基类析构器";
}
void BaseClass::doSomething()
{
std::cout<<"我做了一些事情";
}
Subclass::Subclass()
{
std::cout<<"我进入了子类构造器";
}
Subclass::~Subclass()
{
std::cout<<"我进入了子类析构器";
}
int main()
{
Subclass subclass;
subclass.doSomething();
std::cout<<"收工";
return 0;
};
注意问题
1.初学者常犯的一种错误是用一个毫不相关的类去派生另一个毫不相干的子类。
基本原则:基类跟子类之间的关系应该自然跟清晰。
2.构造器的设计越简明越好,我们应该只用它来初始化各种有关的属性。
基本原则:在设计、定义、使用一个类的时候,应该让他每个组成部分简单到不能再简单。
3.析构器的基本用途是对前面所做的事情进行清理。尤其是动态内存的程序里,析构器尤为重要。
访问控制
所谓访问控制,就是C++提供的一种用来保护类的方法和属性的手段。
访问级别:
public | 任何代码 |
---|---|
protected | 这个类本身跟它的子类 |
private | 只有这个类本身 |
覆盖(overriding)方法
#include<iostream>
#include<string>
class Animal
{
public:
Animal(std::string thename);
void eat();
void sleep();
void drool();
protected:
std::string name;
};//基类:共有的方法
class Pig:public Animal
{
public:
Pig(std::string thename);
void climb();
void eat(); //覆盖 子类再一次声明
};//子类
class Turtle:public Animal
{
public:
Turtle(std::string thename);
void swim();
void eat(); //覆盖 子类再一次声明
};//子类
Animal::Animal(std::string thename)
{
name=thename;
}
void Animal::eat()
{
std::cout<<"i am eating "<<std::endl;
}
void Animal::sleep()
{
std::cout<<"i am sleeping "<<std::endl;
}
void Animal::drool()
{
std::cout<<"i am man "<<std::endl;
}
Pig::Pig(std::string thename):Animal(thename)
{}
void Pig::climb()
{
std::cout<<"i am climbing "<<std::endl;
}
void Pig::eat() //子类实现
{
Animal::eat();
std::cout<<"i am eating grass "<<std::endl;
}
Turtle::Turtle(std::string thename):Animal(thename)
{}
void Turtle::swim()
{
std::cout<<"i am swimming "<<std::endl;
}
void Turtle::eat() //子类实现
{
Animal::eat(); //调用基类eat方法
std::cout<<"i am eating fish "<<std::endl; //
}
int main()
{
Pig pig("zmm");
Turtle turtle("xbzz");
pig.eat();
turtle.eat();
pig.climb();
turtle.swim();
return 0;
}
重载方法
简化编程工作和提高代码可读性的另一种的方法是对方法进行重载。
重载机制使你可以定义多个同名的方法(函数),只是他们输入参数必须不同。(因为编译器是依靠不同的输入参数来区分不同的方法)
#include<iostream>
#include<string>
class Animal
{
public:
Animal(std::string thename);
void eat();
void eat(int eatcount); //重载
void sleep();
void drool();
protected:
std::string name;
};//基类:共有的方法
class Pig:public Animal
{
public:
Pig(std::string thename);
//void eat(int eatcount); 继承在子类不可以重载
void climb();
};//子类
class Turtle:public Animal
{
public:
Turtle(std::string thename);
void swim();
};//子类
Animal::Animal(std::string thename)
{
name=thename;
}
void Animal::eat()
{
std::cout<<"i am eating "<<std::endl;
}
void Animal::eat(int eatcount) //重载实现
{
std::cout<<"wochile"<<eatcount<<"wan hundun!\n";
}
void Animal::sleep()
{
std::cout<<"i am sleeping "<<std::endl;
}
void Animal::drool()
{
std::cout<<"i am man "<<std::endl;
}
Pig::Pig(std::string thename):Animal(thename)
{}
void Pig::climb()
{
std::cout<<"i am climbing "<<std::endl;
}
Turtle::Turtle(std::string thename):Animal(thename)
{}
void Turtle::swim()
{
std::cout<<"i am swimming "<<std::endl;
}
int main()
{
Pig pig("zmm");
Turtle turtle("xbzz");
pig.eat();
turtle.eat();
pig.eat(15); //重载eat输出
pig.climb();
turtle.swim();
return 0;
}
友元关系
友元关系是类之间的一种特殊关系,这种关系不仅允许友元类访问对方的Public方法和属性,还允许友元访问对方的protected和private方法和属性。
声明一个友元关系的语法很简单,只要在类声明里的某个地方加上一条friend class classname 就行了。注意:这条语句可以放在任何地方,放在public 、protected、private段落里都可以。
class Animal
{
public:
Animal(std::string thename);
void eat();
void sleep();
void drool();
protected:
std::string name;
friend class other;//友元
};
静态属性跟静态方法
越高级的语言封装越好。
全局变量可以充当计数器。但是容易引起漏洞。
使用静态属性和静态函数就可以和解决这个问题。
创建一个静态属性和静态方法:只要在他的声明前加上static保留字即可。
#include<iostream>
#include<string>
class Pet
{
public:
Pet(std::string thename);
~Pet();
static int getcount(); //作为一个接口,获取计数器 静态方法
protected:
std::string name;
private:
static int count; //静态属性
};
class Dog : public Pet
{
public:
Dog(std::string thename);
};
class Pig : public Pet
{
public:
Pig(std::string thename);
};
int Pet::count = 0; //做了两件事,1.编译器为count分配内存,2.变量初始化为0
Pet::Pet(std::string thename)
{
name=thename;
count++;
std::cout<<"一只宠物出生了,他的名字是: "<<"\n";
}
Pet::~Pet()
{
count--;
std::cout<<"挂了 "<<"\n";
}
int Pet::getcount()
{
return count;
}
Dog::Dog(std::string thename ) : Pet(thename)
{}
Pig::Pig(std::string thename ) : Pet(thename)
{}
int main()
{
Dog dog ("tom");
Pig pig ("larry");
std::cout<<"\n 诞生"<<Pet::getcount()<<"只宠物!!!"<<"\n";
{
Dog dog2 ("tom2");
Pig pig2 ("larry2");
std::cout<<"\n 现在诞生"<<Pet::getcount()<<"只宠物!!!"<<"\n";
}//作用
std::cout<<"\n And you are back to "<<Pet::getcount()<<"只宠物!!!"<<"\n";
return 0;
};
静态方法(联合this指针)
规则:
1.静态成员是所有对象共享的,所以不能在静态方法里访问非静态的元素
2.非静态的方法可以访问类的静态成员,也可以访问类的非静态成员。
#include<iostream>
class Point
{
private:
int x,y;
public:
Point(int a, int b)
{
x=a;
y=b;
}
void movepoint(int a, int b)
{
x=a;
y=b;
} //声明定义写在一起啦,不建议
void print()
{
std::cout<<" x ="<< x <<" y ="<< y <<std::endl;
}
};
int main()
{
Point point1(10,10);
point1.movepoint(2,2);
point1.print();
return 0;
};
//当对象point1调用movepoint(2,2)函数时,即将point1对象的地址传递给了this指针。
//movepoint函数的原型事实上应该是void movepoint(point *this,int a,int b);
//第一个参数是指向该类对象的一个指针,我们在定义成员函数时没看见是因为这个参数在类中是隐含的。
//这样point1地址传递给了this,所以在movepoint函数中便可以显示的写成:void movepoint(int a, int b){this->x=a;this->y=b;}
//即可以知道,point1调用函数后,也就是point1的数据成员被调用并更新了值。
在任何一个方法里面都可以使用this指针。
在程序运行的时候,对象的属性(变量)和方法(函数)都是保存在内存里的,这就意味着他们各自都有与之相关联的地址。这些地址都可以通过指针来访问,而this指针毋庸置疑是保存着对象本身的地址。
每当调用一个方法的时候,this指针都会随着你提供的输入参数被秘密地传递给那个方法。正是因为如此,我们才能在方法里像使用一个局部变量那样使用this指针。
因为静态方法不是属于某个特定的对象,而是由全体对象共享的,这就意味着他们无法访问this指针。所以我们才无法在静态方法里访问非静态的类成员。
在使用静态属性的时候,千万不要忘记为他们分配内存。具体做法很简单,只要在类声明的外部对静态属性做出声明(就像声明一个变量那样)。
#include<iostream>
#include<string>
class Pet
{
public:
Pet(std::string thename);
~Pet();
static int getcount(); //作为一个接口,获取计数器 静态方法
protected:
std::string name;
private:
static int count; //静态属性
};
class Dog : public Pet
{
public:
Dog(std::string thename);
};
class Pig : public Pet
{
public:
Pig(std::string thename);
};
int Pet::count = 0; //做了两件事,1.编译器为count分配内存,2.变量初始化为0
Pet::Pet(std::string thename)
{
name=thename;
count++;
std::cout<<"一只宠物出生了,他的名字是: "<<"\n";
}
Pet::~Pet()
{
count--;
std::cout<<"挂了 "<<"\n";
}
int Pet::getcount()
{
return count;
}
Dog::Dog(std::string thename ) : Pet(thename)
{}
Pig::Pig(std::string thename ) : Pet(thename)
{}
静态方法也可以使用一个普通方法的调用语句来调用,但不建议这么做,会使代码变得糟糕。
请坚持使用:Classname::methodname();
请不要使用:objectname.methodname();
虚方法 virsual method
引发的问题:使用指向对象的指针
需要我们认识两个新的C++保留字:new和delete
前面我们所讲结果的关于指针的知识,说白了就是一种专门用来保存内存地址的数据。
以前我们的做法是:创建一个变量,再把这个变量的地址赋值给一个指针。然后我们就可以用这个指针访问地址的值。
事实上在C和C++中,我们完全可以在没有创建变量的情况下为有关数据分配内存。也就是直接创建一个指针并让它指向新分配的内存块:
int *pointer = new int; //相当于malloc()
*pointer=110;// 赋值110
std::cout<<*pointer;//打印指针
delete pointer;//删除指针
最后一步很关键,因为程序不会自动释放内存,程序中的每一个new操作都必须有一个与之对应的delete操作。
#include<iostream>
#include<string>
class Pet
{
public:
Pet(std::string thename);
void eat();
void sleep();
void play();
protected:
std::string name;
};
class Cat : public Pet
{
public:
Cat(std::string thename);
void climb();
void play();
};
class Dog : public Pet
{
public:
Dog(std::string thename);
void bark();
void play();
};
Pet::Pet(std::string thename)
{
name=thename;
}
void Pet::eat()
{
std::cout<< name <<"在吃 \n";
}
void Pet::sleep()
{
std::cout<< name <<"在睡觉 \n";
}
void Pet::play()
{
std::cout<< name <<"在玩 \n";
}
Cat::Cat(std::string thename) : Pet(thename)
{
}
void Cat::climb()
{
std::cout<< name <<"在爬树 \n";
}
void Cat::play()
{
Pet::play();
std::cout<< name <<"在玩 \n";
}
Dog::Dog(std::string thename) : Pet(thename)
{
}
void Dog::bark()
{
std::cout<< name <<"在叫 \n";
}
void Dog::play()
{
Pet::play();
std::cout<< name <<"在玩 \n";
}
int main()
{
Pet *cat = new Cat("加菲");
Pet *dog = new Dog("欧弟");
cat -> sleep();
cat -> eat();
cat -> play();
dog->sleep();
dog->eat();
dog->play();
delete cat;
delete dog;
return 0;
};
仔细一看,程序与我们预期不符,我们在Cat和Dog类里对play()方法进行了覆盖,但实际上调用的是Pet::play()方法而不是那两个覆盖的版本。
为什么呢???? -------------使用虚方法
程序所以会有这样奇怪的行为。是因为C++的创始者希望用C++生成的代码至少和它的老前辈C一样快。
所以程序在编译的时候。编译器将检查所有的代码。在如何对某个数据进行处理和可以对该类型的数据进行何种处理之间找个最佳点。
正是这一项编译时的检查影响了刚才的程序结果:cat和dog在编译时都是Pet类型指针,编译器就认为两个指针调用的 play()方法是Pet ::play()方法。因为这是执行起来最快的解决方案。
而引发问题的的源头就是我们使用了new在程序运行的时侯才为dog和cat分配Dog类型和Cat类型的指针。
这些是它们在运行时オ分配的类型,和它们在编译的类型是不一样的!!!
为了让编译器知道它应该根据这两个指针在运行的类型而有选择地调用正确的方法(Dog ::play()和Cat::play())我们必须把这些方法声明为虚方法。
声明一个虚方法的语法非常简单,只要在其原型前加上 virtual保留字即可。
virtual void play();
另外,虚方法是继承的,一旦在基类里把某个方法声明为虚方法,在子类里就不可能再把它声明为一个非虚方法了。
这对于设计程序来说是一件好事。因为这可以让程序员无需顾虑一个虚方法会在某个子类里编程一个非虚方法。
#include<iostream>
#include<string>
class Pet
{
public:
Pet(std::string thename);
void eat();
void sleep();
virtual void play(); // 虚方法
protected:
std::string name;
};
class Cat : public Pet
{
public:
Cat(std::string thename);
void climb();
void play();
};
class Dog : public Pet
{
public:
Dog(std::string thename);
void bark();
void play();
};
Pet::Pet(std::string thename)
{
name=thename;
}
void Pet::eat()
{
std::cout<< name <<"在吃 \n";
}
void Pet::sleep()
{
std::cout<< name <<"在睡觉 \n";
}
void Pet::play()
{
std::cout<< name <<"在玩 \n";
}
Cat::Cat(std::string thename) : Pet(thename)
{
}
void Cat::climb()
{
std::cout<< name <<"在爬树 \n";
}
void Cat::play()
{
Pet::play(); //先调用父类
std::cout<< name <<"在玩玻璃球 \n";
}
Dog::Dog(std::string thename) : Pet(thename)
{
}
void Dog::bark()
{
std::cout<< name <<"在叫 \n";
}
void Dog::play()
{
Pet::play();
std::cout<< name <<"在玩泥巴 \n";
}
int main()
{
Pet *cat = new Cat("加菲");
Pet *dog = new Dog("欧弟");
cat -> sleep();
cat -> eat();
cat -> play();
dog->sleep();
dog->eat();
dog->play();
delete cat;
delete dog;
return 0;
};
小技巧:
1.如果拿不准要不要把某个方法声明为虚方法,那么就把他声明为虚方法就好。
2.在基类里把所有的方法都声明为虚方法会让最终生成的可执行的代码的速度变的稍微慢一点,但好处是一劳永逸的确保程序的行为符合你的预期。
3.在实现一个多层次的类继承关系的时候,最顶级的基类应该只有虚方法。
4.有件事现在可以告诉大家了:析构器都是虚方法!!!从编译的角度看,它们只是普通的方法。如果它们不是虚方法,编译器就会根据它们在编译时的类型而用那个在基类里定义的版本(构造器)・那样往会导致内存泄露!!!!
抽象方法
抽象方法(abstract method,也可以成为纯虚函数)是面向对象编程技术的另一个核心概念,在设计一个多层次的类继承关系时常会用到。
把某个方法声明成一个抽象方法等于告诉编译器这个方法必不可少,但我现在(在这个基类)还不能为他提供一个实现。
其实在我们之前就已经见过一个应该被声明为抽象方法的好例子,没错,就是Pet::play()方法。
抽象方法的语法很简单:在声明虚方法的基础上,在原行的末尾加上”=0“.(告诉编译器不用浪费时间在这个类里寻找这个方法的实现)。
#include<iostream>
#include<string>
class Pet
{
public:
Pet(std::string thename);
virtual void eat();
virtual void sleep();
virtual void play()=0; // 抽象方法
protected:
std::string name;
};
class Cat : public Pet
{
public:
Cat(std::string thename);
void climb();
void play();
};
class Dog : public Pet
{
public:
Dog(std::string thename);
void bark();
void play();
};
Pet::Pet(std::string thename)
{
name=thename;
}
void Pet::eat()
{
std::cout<< name <<"在吃 \n";
}
void Pet::sleep()
{
std::cout<< name <<"在睡觉 \n";
}
void Pet::play()
{
std::cout<< name <<"在玩 \n";
}
Cat::Cat(std::string thename) : Pet(thename)
{
}
void Cat::climb()
{
std::cout<< name <<"在爬树 \n";
}
void Cat::play()
{
Pet::play();
std::cout<< name <<"在玩玻璃球 \n";
}
Dog::Dog(std::string thename) : Pet(thename)
{
}
void Dog::bark()
{
std::cout<< name <<"在叫 \n";
}
void Dog::play()
{
Pet::play();
std::cout<< name <<"在玩泥巴 \n";
}
int main()
{
Pet *cat = new Cat("加菲");
Pet *dog = new Dog("欧弟");
cat -> sleep();
cat -> eat();
cat -> play();
dog->sleep();
dog->eat();
dog->play();
delete cat;
delete dog;
return 0;
};
多态性
多态性是面向对象程序设计的重要特征之—。
简单的说,多态性是指用一个名字定义不同的函数,调用同一个名字的函数,却执行不同的操作,从而实现传说中的"一个接口,多种方法”!
多态是如何实现绑定的?
一编译时的多态性:通过重载实现
一运行时的多态性:通过虚函数实现
编译时的多态性特点是运行速度快,运行时的特点是高度灵活和抽象。
析构函数解析
对于之前说的析构器都是虚方法解释:
#include<iostream>
class Clxbase
{
public:
Clxbase()
{
};
virtual~Clxbase()
{
};
virtual void dosomething()
{
std::cout<<"do something in class clxbase \n ";
}
};
class Clxderived:public Clxbase
{
public:
Clxderived()
{
};
~Clxderived()
{
std::cout<<"output from the destructor of class clxderived \n ";
};
void dosomething()
{
std::cout<<"do something in class clxbase \n ";
};
};
int main()
{
Clxbase *ptest = new Clxderived;//NEW出来的Clxderived对象给到ptest
ptest ->dosomething();//ptest指向dosomething
delete ptest;
return 0;
};
如果把类Clxbase析构函数前面的virtual去掉结果是:do something in class clxbase
#include<iostream>
class Clxbase
{
public:
Clxbase()
{
};
~Clxbase()//去掉virtual
{
};
virtual void dosomething()
{
std::cout<<"do something in class clxbase \n ";
}
};
class Clxderived:public Clxbase
{
public:
Clxderived()
{
};
~Clxderived()
{
std::cout<<"output from the destructor of class clxderived \n ";
};
void dosomething()
{
std::cout<<"do something in class clxbase \n ";
};
};
int main()
{
Clxbase *ptest = new Clxderived;
ptest ->dosomething();
delete ptest;
return 0;
};
一般下类的析构函数里面都是释放内存资源,而析构函数不被调用的话依旧会造成内存泄露。
所以,析构器都是虚方法是为了当一个基类的指针删除一个派生类的对象时,派生类的析构函数可以被正确调用。
另外,当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里边存放着虚函数指针。
为了节省资源,只有当一个类被用来作为基类的时候,我们才把析构函数写成虚函数!!!!
运算符重载
所谓重载,就是重新赋予新的含义。
函数重载是对一个已有的函数赋予新的含义,使之实现新功能。
其实运算符也可以重载,实际上,我们常常在不知不觉之中使用了运算符重载。
运算符重载的方法是定义一个重载运算符的函数,在需要执行被重载的运算符时,系统就自动调用该函数,以实现相应的运算。
也就是说,运算符重载是通过定义函数实现的。
运算符重载实质上是函数的重载。
重载运算符的函数格式一般如下:
函数类型 operator 运算符名称(形参列表) //operator 是关键词
{
对运算符的重载
}
举个例子:实现复数的加法
(3,4i)+(5,-10i)=(8,6i)
当我们不知道重载的时候,我们可能这么做:
#include<iostream>
class Complex //复数
{
private:
double real;//shibu
double imag;//xubu
public:
Complex();
Complex(double r,double i);
Complex complex_add(Complex &d); //加法方法
void print();
};
Complex::Complex()
{
real=0;
imag=0;
}
Complex::Complex(double r,double i)
{
real=r;
imag=i;
}
Complex Complex::complex_add(Complex &d)
{
Complex c;
c.real=real+d.real;
c.imag=imag+d.imag;
return c;
}
void Complex::print()
{
std::cout<<"("<<real<<","<<imag<<"i)\n";
}
int main()
{
Complex c1(3,4),c2(5,-10),c3;
c3=c1.complex_add(c2);
std::cout<<"c1= ";
c1.print();
std::cout<<"c2= ";
c2.print();
std::cout<<"c1+c2= ";
c3.print();
return 0;
};
当我们知道重载时,我们会这么做:
#include<iostream>
class Complex //复数
{
private:
double real;//shibu
double imag;//xubu
public:
Complex();
Complex(double r,double i);
Complex operator+(Complex &d); //重载 对+号进行重载
void print();
};
Complex::Complex()
{
real=0;
imag=0;
}
Complex::Complex(double r,double i)
{
real=r;
imag=i;
}
Complex Complex::operator+(Complex &d) //重载实现
{
Complex c;
c.real=real+d.real;
c.imag=imag+d.imag;
return c;
}
void Complex::print()
{
std::cout<<"("<<real<<","<<imag<<"i)\n";
}
int main()
{
Complex c1(3,4),c2(5,-10),c3;
c3=c1+c2;//重载之后的变化
std::cout<<"c1= ";
c1.print();
std::cout<<"c2= ";
c2.print();
std::cout<<"c1+c2= ";
c3.print();
return 0;
};
我们在声明Complex累的时候对运算符进行了重载,使得这个类在编译的时候可以完全不考虑函数是如何实现的,直接使用+、-、*、/、进行复数运算。
其实,我们还可以写的更精简一些:
Complex Complex::operator+(Complex &c2)
{
return Complex(real+c2.real,imag+c2.imag);
}
C++不允许用户自己定义新的运算符。只能对其进行重载。其中有五个运算符不能重载,其他的都能。
. | 成员访问运算符 |
---|---|
* | 成员指针访问运算符 |
:: | 域运算符 |
sizeof | 尺寸运算符 |
? | 条件运算符 |
重载的一些规则
重载不能改变运算符运算对象(操作数)个数。
重载不能改变运算符的优先级别。
重载不能改变运算符的结合性。
重载运算符的函数不能有默认的参数。
重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应该有一个是类对象或类对象的引用。(也就是说,参数不能全部都是C++的标准类型,这样约定是为了防止用户修改用于标准类型结构的运算符性质)。
运算符重载函数作为友元函数
不知道刚刚有没有人有这样的疑问:“+“运算符是双目运算符,为什么刚刚的例子中的重载函数只有一个参数呢?
解答:实际上,运算符重载函数有两个参数,但由于重载函数是Complex 类中的成员函数,有一个参数是隐含着的,运算符函数是用this指针隐式地访问类对象的成员。
return Complex( real+ c2. real, imag+c2.imag);
return Complex( this->real+ c2. real, this->imag+c2.imag);
return Complex( c1.real+c2.real, c1.imag+ c2.imag );
//那么例子中的c1+c2,编译系统把它解释为:c1.operator+(c2)
即通过对象c1调用运算符重载函数,并以表达式中第二个参数(运算符右侧的类对象c2)作为函数实参。
运算符重载函数除了可以作为类的成员函数外,还可以是非成员函数:放在类外,做Complex 类的友元函数存在:
#include<iostream>
class Complex //复数
{
private:
double real;//shibu
double imag;//xubu
public:
Complex();
Complex(double r,double i);
friend Complex operator+ (Complex &c,Complex &d ); //重载 对+号进行重载 友元函数
void print();
};
Complex::Complex()
{
real=0;
imag=0;
}
Complex::Complex(double r,double i)
{
real=r;
imag=i;
}
Complex operator+ (Complex &c,Complex &d) //注意这里作为友元函数,不属于Complex记得别写:: cC1 dC2
{
return Complex(c.real+d.real, c.imag+ d.imag);
}
void Complex::print()
{
std::cout<<"("<<real<<","<<imag<<"i)\n";
}
int main()
{
Complex c1(3,4),c2(5,-10),c3;
c3=c1+c2;//重载之后的变化
std::cout<<"c1= ";
c1.print();
std::cout<<"c2= ";
c2.print();
std::cout<<"c1+c2= ";
c3.print();
return 0;
};
运算符重载函数作为类友元函数
为什么把运算符函数作为友元函数呢?
因为运算符函数要访问Complex 类对象的成员,如果运算符函数不是Complex 类的友元函数,而是一个普通的函数,它是没有权力访问Complex 类的私有成员的。
由于友元的使用会破坏类的封装,因此从原则上说,要尽量将运算符函数作为成员函数。
请听题:
重载运算符"+","-","*","/"实现有理数的加减乘除运算。
如1/8+7/8=1;
如1/8-7/8=-6/8
如1/8*7/8=7/64
如1/8/7/8=1/7
#include<iostream>
#include<stdlib.h>
#include<string>
class Rational
{
//有理数
public:
Rational(int num,int denom); //num是分子,denom是分母 构造函数
Rational operator+(Rational rhs ); // rhs===right hand side
Rational operator-(Rational rhs );
Rational operator*(Rational rhs );
Rational operator/(Rational rhs );
void print();//声明了一个打印的方法
private:
void normalize();//负责对分数简化处理
int numerator;//分子
int denominator;//分母
};
Rational::Rational(int num,int denom)
{
numerator=num;
denominator=denom;
normalize();
}
//normalize()对分数进行简化操作包括
//1.只允许分子为负数,如果分母为负数则把负数挪到分子的部分,如1/-2=-1/2
//2.利用欧几里得算法(辗转求余原理)将分数进行简化:2/10=1/5
void Rational::normalize()
{
if (denominator<0)
{
numerator=-numerator;
denominator=-denominator;
}//确保分子是正的
int a=abs(numerator);
int b=abs(denominator);//欧几里得算法
while (b>0)
{
int t=a%b;
a=b;
b=t;
}//求最大公约数
numerator/=a;
denominator/=a;//分子分母除以最大公约数得到最简化分数
}
Rational Rational::operator+(Rational rhs)
{
int a=numerator;
int b=denominator;
int c=rhs.numerator;
int d=rhs.denominator;
int e=a*b+c*d;
int f=b*d;
return Rational(e,f);
}
Rational Rational::operator-(Rational rhs)
{
rhs.numerator = -rhs.numerator;
return operator+(rhs);
}
Rational Rational::operator*(Rational rhs)
{
int a=numerator;
int b=denominator;
int c=rhs.numerator;
int d=rhs.denominator;
int e=a*c;
int f=b*d;
return Rational(e,f);
}
Rational Rational::operator/(Rational rhs)
{
int t=rhs.numerator;
rhs.numerator=rhs.denominator;
rhs.denominator=t;
return operator*(rhs);
}
void Rational::print()
{
if(numerator%denominator==0)
std::cout<<numerator/denominator;
else
std::cout<<numerator<<"/"<<denominator;
}
int main()
{
Rational f1(2,16);
Rational f2(7,8);
//测试加法
Rational res=f1+f2;
f1.print();
std::cout<<"+";
f2.print();
std::cout<<"=";
res.print();
std::cout<<"\n";
//测试减法
Rational w=f1-f2;
f1.print();
std::cout<<"-";
f2.print();
std::cout<<"=";
w.print();
std::cout<<"\n";
//测试乘法
Rational a=f1*f2;
f1.print();
std::cout<<"*";
f2.print();
std::cout<<"=";
a.print();
std::cout<<"\n";
//测试除法
Rational x=f1/f2;
f1.print();
std::cout<<"/";
f2.print();
std::cout<<"=";
x.print();
std::cout<<"\n";
};
重载<<操作符
上面的代码你满意吗??反正我是觉得那些代码的可读性仍然欠佳:main函数里边要多次调用print方法才能实现分数打
印,酱紫不行!
这节课教大家如何通过重载<<操作符来实现print 打印分数的功能。《官方的叫插入器》
从第一次输出值开始<<操作符就一窗被重载.
例如:std:cout<<"Hello Fishc !"
。C标准库对左移操作符(<<)进行了重载,让它可以把值发送到一个流去。(流来流去的概念)
但是在这个例子中,iostream 库对新的Rational 类表示一无所知,所以不能直接用<<来输出我们的有理数(分数)。
但是,没有什么能够阻挡我们重载<<操作符来让它接受Rational 对象的宏伟愿望!!
另外一个原因也比较重要:因为,重载的含义本身就是可以用相同的名字去实现不同的功能:输入参数方面有所差异就不会有问题。
当然,我们无法在现有的ostream 类里专门添加一个新的operator <<()方法。
所以我们只能定义一个正常的函数在外部重载这个操作符,这与重载方法的语法大同小异、唯一的区别是不再有一个对象可以用来调用<<重载函数,而不得不通过第一个输入参数向这个重载方法传递对象。
注意区别前面我们对四则运算的重载。
下面是一个operator<<()函数的原型:
std: : ostream& operator< < ( std: : ostream& os Rational f );
第一个输入参数os是将要向它写数据的那个流,它是以“引用传递”方式传递的。
第二个输入参数是打算写到那个流里的数据值·不同的operator<<()重载函数就是因为这个输入参数才相互区别的。
返回类型是ostream 流的引用。一般来说,在调用operator<<()重载函数时传递给它的是哪-个流,它返回的就应该是那个流的一个引用。
好了,介绍就说这么多。我们对上面程序进行改进。
#include<iostream>
#include<stdlib.h>
#include<string>
class Rational
{
//有理数
public:
Rational(int num,int denom); //num是分子,denom是分母 构造函数
Rational operator+(Rational rhs ); // rhs===right hand side
Rational operator-(Rational rhs );
Rational operator*(Rational rhs );
Rational operator/(Rational rhs );
void print();//声明了一个打印的方法
private:
void normalize();//负责对分数简化处理
int numerator;//分子
int denominator;//分母
friend std::ostream& operator<<(std::ostream& os, Rational f);//改进重载左移操作符
};
Rational::Rational(int num,int denom)
{
numerator=num;
denominator=denom;
normalize();
}
//normalize()对分数进行简化操作包括
//1.只允许分子为负数,如果分母为负数则把负数挪到分子的部分,如1/-2=-1/2
//2.利用欧几里得算法(辗转求余原理)将分数进行简化:2/10=1/5
void Rational::normalize()
{
if (denominator<0)
{
numerator=-numerator;
denominator=-denominator;
}//确保分子是正的
int a=abs(numerator);
int b=abs(denominator);//欧几里得算法
while (b>0)
{
int t=a%b;
a=b;
b=t;
}//求最大公约数
numerator/=a;
denominator/=a;//分子分母除以最大公约数得到最简化分数
}
Rational Rational::operator+(Rational rhs)
{
int a=numerator;
int b=denominator;
int c=rhs.numerator;
int d=rhs.denominator;
int e=a*b+c*d;
int f=b*d;
return Rational(e,f);
}
Rational Rational::operator-(Rational rhs)
{
rhs.numerator = -rhs.numerator;
return operator+(rhs);
}
Rational Rational::operator*(Rational rhs)
{
int a=numerator;
int b=denominator;
int c=rhs.numerator;
int d=rhs.denominator;
int e=a*c;
int f=b*d;
return Rational(e,f);
}
Rational Rational::operator/(Rational rhs)
{
int t=rhs.numerator;
rhs.numerator=rhs.denominator;
rhs.denominator=t;
return operator*(rhs);
}
void Rational::print()
{
if(numerator%denominator==0)
std::cout<<numerator/denominator;
else
std::cout<<numerator<<"/"<<denominator;
}
std::ostream& operator<<(std::ostream& os, Rational f); //函数声明
int main()
{
Rational f1(2,16);
Rational f2(7,8);
std::cout<<f1<<"+"<<f2<<"=="<<(f1+f2)<<"\n";
std::cout<<f1<<"-"<<f2<<"=="<<(f1-f2)<<"\n";
std::cout<<f1<<"*"<<f2<<"=="<<(f1*f2)<<"\n";
std::cout<<f1<<"/"<<f2<<"=="<<(f1/f2)<<"\n";
return 0;
};
std::ostream& operator<<(std::ostream& os, Rational f) //函数定义 因为它不属于上面的类,是一个单独的函数。
{
os<<f.numerator<<"/"<<f.denominator;
return os;
}
多继承
多继承(multiple inheritance))可能是面向对象编程技术中最惹人争议的功能了。
这个概念乍看起来很简单,但它可能引起一些难以预料的后果。
因此JAVA和C#等面向对象编程语言大都只支持多继承的最简单的版本。
什么时候需要用到多继承?
只要你遇到的问题无法只用一个“是一个“关系来描述的时候,就是多继承出场的时候。
举个例子:在学校里有老师和学生,他们都是人(Person )·我们可以用“老师是人“和”学生是人“语法来描述这种情况。
从面相对象编程角度上来看·我应该创建一个名为Person t的基类和两个名为Teacher 和Student 的子类,后两者是从前者继承来的。
问题来了:有一部分学生还教课挣钱(助教),该怎么办?酱紫就存在了既是老师又是学生的复杂关系,也就是同时存在着两个"是一个“关系。
我们需要写一个Teaching Student 类让它同时继承Teacher 类和Student 类,换句话说,就是需要使用多继承。
基本语法:
class TeachingStudent : public Student, public Teacher
{.........}
下边我们写个示例演示一下这个多继承的模型!!!
要求:创建一个由Person ,Teacher ,Student 和Teaching Student 构成的类层次结构。
#include<iostream>
#include<string>
class Person
{
public:
Person(std::string thename);
void introduce();
protected:
std::string name;
};
class Teacher:public Person
{
public:
Teacher(std::string thename,std::string theclass);
void teach();
void introduce();
protected:
std::string classes;
};
class Student:public Person
{
public:
Student(std::string thename,std::string theclass);
void attendclass();
void introduce();
protected:
std::string classes; //不能跟类名重复
};
class Teachingstudent:public Student, public Teacher //多继承 zhujiao
{
public:
Teachingstudent(std::string thename,std::string classteaching,std::string classattending );
void introduce();
};
Person::Person(std::string thename)
{
name=thename;
}
void Person::introduce()
{
std::cout<<"大家好,我是"<<name<<"\n";
}
Teacher::Teacher(std::string thename,std::string theclass):Person(thename)
{
classes=theclass;
}
void Teacher::teach()
{
std::cout<<name<<"教"<<classes<<"\n";
}
void Teacher::introduce()
{
std::cout<<"大家好,我是"<<name<<",我教"<<classes<<"\n";
}
Student::Student(std::string thename,std::string theclass):Person(thename)
{
classes=theclass;
}
void Student::attendclass()
{
std::cout<<"大家好,我是"<<name<<",加入"<<classes<<"学习\n";
}
void Student::introduce()
{
std::cout<<"大家好,我是"<<name<<",我在"<<classes<<"学习\n";
}
Teachingstudent::Teachingstudent(std::string thename,std::string classteaching,std::string classattending ):Teacher(thename,classteaching),Student(thename,classattending)
{
}
void Teachingstudent::introduce()
{
std::cout<<"大家好,我是"<<Student::name<<",我教"<<Teacher::classes;
std::cout<<"同时,我在"<<Student::classes<<"学习\n";
}
int main()
{
Teacher teacher("李老师","数学班");
Student student("李明","数学班");
Teachingstudent teachingstudent("阿拉蕾","数学班","语文班");
teacher.introduce();
teacher.teach();
student.introduce();
student.attendclass();
teachingstudent.introduce();
teachingstudent.teach();
teachingstudent.attendclass();
return 0;
}
虚继承
前一节的student 示例程序看起来似乎已经解决了问题,但它存在着一些隐患。首先,在Teaching Student 类的introducel()方
法里,我们不得不明确地告诉编译器应该使用哪一个属性。
这对于classes 属性来说是应该的,因为教一门课和上一门课有着本质的区别,而作为常识,助教生教的课程和他学的课程不可能一样!!!
但是我们再深入考虑下,既然在Teaching Student 对象里可以继承两个不同的classes 属性,那它是不是应该有两个不同的name属性呢?
答案:是! 事实上,Teaching Student 还真可以有两个不同的名字,这肯定不是我们在设计这个类继承模型时所预期的:
#include<iostream>
#include<string>
class Person
{
public:
Person(std::string thename);
void introduce();
protected:
std::string name;
};
class Teacher:public Person
{
public:
Teacher(std::string thename,std::string theclass);
void teach();
void introduce();
protected:
std::string classes;
};
class Student:public Person
{
public:
Student(std::string thename,std::string theclass);
void attendclass();
void introduce();
protected:
std::string classes; //不能跟类名重复
};
class Teachingstudent:public Student, public Teacher //多继承 zhujiao
{
public:
Teachingstudent(std::string thename,std::string classteaching,std::string classattending );
void introduce();
};
Person::Person(std::string thename)
{
name=thename;
}
void Person::introduce()
{
std::cout<<"大家好,我是"<<name<<"\n";
}
Teacher::Teacher(std::string thename,std::string theclass):Person(thename)
{
classes=theclass;
}
void Teacher::teach()
{
std::cout<<name<<"教"<<classes<<"\n";
}
void Teacher::introduce()
{
std::cout<<"大家好,我是"<<name<<",我教"<<classes<<"\n";
}
Student::Student(std::string thename,std::string theclass):Person(thename)
{
classes=theclass;
}
void Student::attendclass()
{
std::cout<<"大家好,我是"<<name<<",加入"<<classes<<"学习\n";
}
void Student::introduce()
{
std::cout<<"大家好,我是"<<name<<",我在"<<classes<<"学习\n";
}
Teachingstudent::Teachingstudent(std::string thename1,std::string thename2,std::string classteaching,std::string classattending ):Teacher(thename1,classteaching),Student(thename2,classattending)//更改了这里name1、2
{
}
void Teachingstudent::introduce()
{
std::cout<<"大家好,我是"<<Student::name<<",我教"<<Teacher::classes;
std::cout<<"同时,我在"<<Student::classes<<"学习\n";
}
int main()
{
Teacher teacher("李老师","数学班");
Student student("李明","数学班");
Teachingstudent teachingstudent("丁丁","丹丹","数学班","语文班");//更改之后,有点人格分裂。
teacher.introduce();
teacher.teach();
student.introduce();
student.attendclass();
teachingstudent.introduce();
teachingstudent.teach();
teachingstudent.attendclass();
return 0;
}
Teaching Student 类继承自Teacher 和Student 两个类,因而继承了两组Person 类的属性·这在某些时候完全有道理·例如classes属性。
但它也有可能引起麻烦,例如发生在name属性身上的情况。
C++发明者也想到了这部分的冲突,因此为此提供了一个功能可以解决这个问题:虚继承(virtual inheritance)。
通过虚继承某个基类,就是在告诉编译器:从当前这个类再派生出来的子类只能拥有那个基类的一个实例。
虚继承的语法:
class Teacher: virtual public Person
{........}
这样做我们的问题就解决了:让Student 和Teacher 类都虚继承自Person 类,编译器将确保从Student 和Teacher 类再派生出来的子类只能拥有一份Person 类的属性!
栗子修改:
#include<iostream>
#include<string>
class Person
{
public:
Person(std::string thename);
void introduce();
protected:
std::string name;
};
class Teacher:virtual public Person //虚继承
{
public:
Teacher(std::string thename,std::string theclass);
void teach();
void introduce();
protected:
std::string classes;
};
class Student:virtual public Person //虚继承
{
public:
Student(std::string thename,std::string theclass);
void attendclass();
void introduce();
protected:
std::string classes; //不能跟类名重复‘【
};
class Teachingstudent:public Student, public Teacher //多继承 zhujiao
{
public:
Teachingstudent(std::string thename,std::string classteaching,std::string classattending );
void introduce();
};
Person::Person(std::string thename)
{
name=thename;
}
void Person::introduce()
{
std::cout<<"大家好,我是"<<name<<"\n";
}
Teacher::Teacher(std::string thename,std::string theclass):Person(thename)
{
classes=theclass;
}
void Teacher::teach()
{
std::cout<<name<<"教"<<classes<<"\n";
}
void Teacher::introduce()
{
std::cout<<"大家好,我是"<<name<<",我教"<<classes<<"\n";
}
Student::Student(std::string thename,std::string theclass):Person(thename)
{
classes=theclass;
}
void Student::attendclass()
{
std::cout<<"大家好,我是"<<name<<",加入"<<classes<<"学习\n";
}
void Student::introduce()
{
std::cout<<"大家好,我是"<<name<<",我在"<<classes<<"学习\n";
}
Teachingstudent::Teachingstudent(std::string thename,std::string classteaching,std::string classattending ):Teacher(thename,classteaching),Student(thename,classattending),Person(thename)//增加Person,调用它的构造语法
{
}
void Teachingstudent::introduce()
{
std::cout<<"大家好,我是"<<name<<",我教"<<Teacher::classes; // 修改了这里由Student name 变为 name
std::cout<<"同时,我在"<<Student::classes<<"学习\n";
}
int main()
{
Teacher teacher("李老师","数学班");
Student student("李明","数学班");
Teachingstudent teachingstudent("ala","数学班","语文班");
teacher.introduce();
teacher.teach();
student.introduce();
student.attendclass();
teachingstudent.introduce();
teachingstudent.teach();
teachingstudent.attendclass();
return 0;
}
错误处理及调试
编译时错误(complie–time error )
程序出错可以分为两大类:编译时错误(complie–time error )和运行时错误(run-time error)。
相比之下,编译时错误显然是比较轻的。因为编译将会告诉你它发现了什么错误和它是在哪行代码发现了这个错误的。
我们需要做的只是认真观察和分析编译器给出的出错信息,然后按语法要求改正即可。
下边总结一些编程好经验给大家参考:
建议一:培养并保持一种编程风格!
第一个建议是在编程的时候保持一种风格,一旦决定了要如何命名变量和函数、要按何种格式编写代码、如何缩进代码块等,就应该一直保持下去。
建议二:认真对待编译器给出的错误/警告信息。
有时候,编译器给出的警告信息完全没道理,但大多数时候还是很有用的,虽然警告不影响程序编译,但千万不要忽视它们。
建议三:三思而后行
开始写代码前先画流程图。
编译错误不要立刻修改源代码,应该先完整地审阅一遍源代码,再开始纠正错误。因为冒失地修改源代码往往会造成错误越改越多丶心情越改越乱的结局。
建议四:注意检查最基本的语法
再有经验的妞也会上钩·再有经验的程序员也同样会犯一些小错误。
建议五:把可能有问题的代码行改为注释
不要轻易整行整行地删除代码,把可能有问题的代码行先改成注释,看错误是否还在。排除法。。。
建议六:换一个环境或开发互具试试
—般来说·编译器不会有问题,但如果你始终无法确定问题出在哪里,不妨换一下编译器或者操作系统。常常有时候弱智的杀软会阻止编译器导致编译失败。
建议七:检查自己是否已经把所有必要的头文件全部include 进来。
例如只有#include <iostream>>才能使用cout.与此相类似的情况成百上千,并容易忽略,注意调用不熟悉的函数前查看关文档,确定该函数需要哪些头文件支持。
建议八:留意变量的作用域和命名空间
程序代码对变量的访问权限可能导致各种各样的问题,这个知识今后我们会深入探讨。
建议九:休息一下!
在情绪变得越来越焦躁的时候,你发现和解决问题的能力会直线下降。这时候应该让自己放松一下。离开计算机,等头脑清醒了再回来解决问题,做开发的时候也是同样哦
建议十:使用调试互具
绝大多数IDE都有一个内建的调试器,一定要学习使用它并经常使用它。
最后,避免错误的另一个好方法就是把调试好的代码另外保存起来并不再改动它。然后把代码划分成各个模块,用它们(在你能保证它们都没有问题的情况下)来搭建新的应用程序,会让你减少很多开发和调试的时间。
运行时错误(run-time error)
运行时错误往往远比编译时错误更难以查找和纠正,运行时错误一般都不会有正式的出错信息。它们的发生几率因不同程序思路不同而不同,很少有规律可循。更多的情况是时有时无,有的程序在这台计算机上很正常,在另一台计算机上就总是出问题。或者某几个用户经常遇到这样或那样的问题,其他用户却都正常。运行时错误的外在表现可以说是千变万化!
经验一:还是培养并保持一种良好的编程风格!
杂乱无章是程序员最大的敌人,找到你最喜欢的编程风格然后一直保持下去吧!
经验二:多用注释,用好注释。
如果忘记了某段代码的用途和理由·再想回来调试修改这段代码可就费劲了。这里要注意的是.必须让注释和代码保持同步·一旦修改了代码,就应该对注释进行相应的修改。
注意不要做无谓的注释。
经验三:注意操作符的优先级
操作符的优先级决定着有关操作的发生顺序·如果想让一系列操作按照希望的顺序发生,最保险的方法是用括号来确保这种顺序。
此处应该有例子:
#include<iostream>
int main()
{
int a=1,b;
if(b=a--) //此处将a的值 -1赋值给b
{
std::cout<<"yeah !\n";
}
else
{
std::cout<<"out !\n";
}
return 0;
}
另外,不要对操作顺序做任何假设--------在某些场合++,*和->之类的高优先级操作符的行为也不见得是你想象那样哦。
经验四:千万不要忘记对用户输入和文件输入进行合法性检查。
如果让某个程序员去调试他本人编写的代码,他往往不能把所有的漏洞全都找出来-------------因为他会下意识地避免各种不正常的做法。
只有用户才会做出一些出乎意料的事情,对于来自用户的输入,一定要采用正确的方法来读取它们和检查它们的合法性,确保你将要处理的数据符合你对它们的要求。
经验五:不要做任何假设
这是一句老生常谈了,但至今仍适用于所有场合。不要想当然地认为一个应该发生的操作比如打开一个文件、创建一个新的内存块,等等。不要想当然地认为用户肯定会按照你的意愿去使用你的程序。
经验六:把程序划分成一些比较小的单元模块来测试
程序越长,就越难以测试,只要条件允许,就应该把一个比较大的程序划分成一系列比较小的单元模块来分别加以测试。
让函数返回错误代码
让程序能够自行处理潜在错误的办法之一是创建一些测试函数 : 专门测试某种条件并根据测试结果返回个代码来表示当前函数的执行状态。
int myFunction(){
if( condition)
{
return 0;
}
else
{
return 1;
}
}
这种方式我们已经了然于心,但我们仍有进一步继续讨论的必要。不知道大家还记不记得很久很久以前,我们一起
写过的那个求阶乘的栗子:
#include<iostream>
unsigned long returnFactorial(unsigned short num);
int main()
{
unsigned short num;
std::cout<<"请输入一个整数: ";
std::cin>>num;
std::cout<<num<<"阶乘数结果是:"<<returnFactorial(num)<<"\n";
return 0;
}
unsigned long returnFactorial(unsigned short num)
{
unsigned long sum = 1;
for (int i = 1; i <= num; i++)
{
sum*=i;
}
return sum;
}
其实这个栗子充满隐患。为什么?我们输入13从表面上看·程序既没有崩溃也没有报告出错,但它已经不正常了!
因为13阶乘的结果不正确。
在知道问题后,我们其实也很容易的就可以猜到错误发生的原因:计算阶乘的值超出了计算机所能表达的最大整数(至少在我们这台机子上)。
鉴于这类问题的纠正,小甲鱼在这里教大家一个新技巧:运用climits 头文件
climits 头文件?
这个头文件从C的limits.h 头文件引用过来的。
主要列出了各种数据类型在给定操作系统上的取值范围,并且把每种数据类型的最大可取值和最小可取值都分别定义为一个常量供我们比较。
比如,SHORT _MAX代表短整数类型在给定系统上的最大可取值,SHORT_MIN 代表短整数类型在给定操作系统上的最小可取值。USHORT _MAX代表无符号整数类型的最大可取值。
在这个程序里·为了判断阶乘计算的结果没有超出一个无符号长整数的最大取值,我们可以使用ULONG _MAX来提前获得这个值进行对比。
一起动手来修改这个代码
#include<iostream>
#include<climits>
class Factorial
{
public:
Factorial(unsigned short num);
unsigned long getFactorial();
bool inrange();
private:
unsigned short num;
};
Factorial::Factorial(unsigned short num)
{
this->num=num;
}
unsigned long Factorial::getFactorial()
{
unsigned long sum =0;
for (int i = 0; i <= num; i++)
{
sum*=i;
}
return sum;
}
bool Factorial::inrange()
{
unsigned long max =ULONG_MAX;
for ( int i = max; i >= 1; --i)
{
max/=i;
}
if (max<1)
return false;
else
return true;
}
int main()
{
unsigned short num =0;
std::cout<<"请输入一个数字:";
std::cin>>num;
Factorial fac(num);
if (fac.inrange())
{
std::cout<<num<<"的阶乘值是"<<fac.getFactorial()<<"\n\n";
}
else
{
std::cout<<"你所输入的值太大";
}
return 0;
}
有些程序员喜欢使用异常(咱们将在稍后学习)而不是使用return 语句。反对使用这个栗子程序里所演示的技巧的主要理由是:把各种出错代码进行处理的语句混杂在程序的主干部分,既不利于模块化编程,又容易干扰正常的思考!
assert函数和捕捉异常
C语言和C+都有一个专为调试而准备的互具函数,就是assert( )函数。这个函数是在C语言的assert .h库文件里定义的,所以包含到C+程序里我们用以下语句:
# include < assert>
assert ()函数需要有一个参数,它将测试这个输入参数的真or假状态。
一如果为真,Do nothing !
一如果为假·Do something !
#include<cassert>
int main()
{
int i=20;
assert(i==65);
return 0;
}
演示中·我们看到assert ()函数可以帮助我们调试程序。我们可以利用它在某个程序里的关键假设不成立时立刻停止该程序的执行并报错·从而避免发生更严重的问题。另外·除了结合assert ()函数·在程序的开发、测试阶段。我们还可以使用大量的cout语句来报告在程序里正在发生的事情。
用户体验与程序员体验
对运行时错误的处理分为两种情况:
一种与程序猿有关,在开发、测试和调试程序的过程中。程序猿需要尽可能详细的信息来查找和纠正各种潜在的运行时错误。
另一种情况与最终用户有关,在使用一个程序的过程中,错误处理应该把用户的感受摆在第一位!
在理想的情况下·程序发布之前,它里边的所有错误都应该被发现和改正过来。只可惜这是几乎不可能的,就连微软这样的大公司也做不出这样的保证!
注意·我们这几节课讨论的内容是从程序猿的角度展开的。这里介绍的一些技巧不应该用在一个即将交付给最终用户的应用程序里。因为这样的用户使用体验会差到极点!
最为一条原则:最终用户看到的错误信息应该既专业又清晰·不能轻易中断程序,不能充满技术细节。
捕获异常
同样为了对付潜在的编程错误(尤其是运行时的错误),捕获异常是一种完全不同的办法。
简单地说,异常(exception)就是与预期不相符合的反常现象。
基本使用思路:
1.安排一些C+代码(try语句)去尝试某件事——尤其是那些可能会失败的事(比如打开一个文件或申请一些内存)
2.发生问题·就抛出一个异常(throw语句)
3.再安排一些代码(catch语句)去捕获这个异常并进行相应的处理。
捕获异常的基本语法如下:
try
{
// Do something
// Throw an exception on error.
}
catch
{
// Do whatever
}
Pay attention !每条try语句至少要有一条配对的catch语句。必须定义catch 语句以便让它接收一个特定类型的参数。
C++还允许我们定义多条catch 语句,让每条catch 语句分别对应着一种可能的异常类型:
catch (int e){…}
catch (bool e){…}
catch (…){…}
最后一条catch语句可以捕获任何类型的异常。
在程序里,我们可以用throw 保留字来抛出一个异常:throw 1 :
在某个try语句块里执行过throw 语句,它后面的所有语句(截止到这个try语句块末尾)将永远不会被执行。
与使用一个条件语句或return 语句相比。采用异常处理机制的好处是它可以把程序的正常功能和逻辑与出错处理部分清晰地划分开而不是让它们混杂在一起。
让函数抛出异常
你可以在定义一个函数时明确地表明你想让它抛出一个异常,为了表明你想让它抛出哪种类型的异常,可以使用如下所示语法:
type functionName(arguments) throw(type);
如果没有使用这种语法来定义函数,就意味着函数可以抛出任意类型的异常。
注:有些编译器不支持这种语法·则可省略throw(type)部分。
#include<iostream>
#include<climits>
unsigned long returnFactorial(unsigned short num) throw (const char *);
int main()
{
unsigned short num =0;
std::cout<<"请输入一个数字:";
while (!(std::cin>>num||(num<1)))
{
std::cin.clear(); //清楚状态
std::cin.ignore(100,'\n');//清楚缓冲区
std::cout<<"请输入一个整数:";
}
std::cin.ignore(100,'\n');
try
{
unsigned long factorial =returnFactorial(num);
std::cout<<num<<"的阶乘是:"<<factorial;
}
catch(const char *e)
{
std::cout<<e;
}
return 0 ;
}
unsigned long returnFactorial(unsigned short num) throw (const char *)
{
unsigned long sum =1;
unsigned long max=ULONG_MAX;
for (int i = 1; i <= num ; i++)
{
sum*=i;
max/=i;
}
if (max<1)
{
throw "太惨了,不能够计算出该值。\n"
}
else
{
return sum;
}
}
小技巧:
如何使用异常是一个很容易引起争论的话题。
有些程序员使用异常来处理几乎所有的错误,但C+的创始人Bjarne stroustrup 觉得它们正在被滥用。
所以使用异常的基本原则是:应该只用它们来处理确实可能不正常的情况。
作为一条原则,在构造器和析构器里不应该使用异常。一位非常有经验的程序猿在这些方法里成功地使用了异常是有可能的,但稍有不慎就会导致严重的问题。
如果try语句块无法找到一个与之匹配的catch 语句块,它抛出的异常将中止程序的执行。
在C++标准库里有个一名为exception 的文件,该文件声明了一个exception 的基类。可以用这个基类来创建个人的子类以管理异常。
有经验的程序猿常常这么做,而如此抛出和捕获的是exception 类或其子类的对象。
如果你打算使用对象作为异常,请记住这样一个原则:以"值传递“方式抛出对象,以"引用传递“方式捕获对象。
动态内存管理
到目前为止不了,给大家所讲解的每一个示例程序在完成它的任务时所使用的内存空间都是固定不变的。
这个固定不变的内存空间其实是在编写程序时候就可以知道和确定(一般以变量的形式)。这些程序都不能在程序运行期间动态增加或减少内存空间。但小时候,性感而卡哇伊的物理老师告诉我们:这个世界没有完全静止的东西!现实世界是动态的!!!所以,C++也必须支持动态管理内存。而这节课正式要跟大家介绍这个。
你见过要求用户输入的文本必须不多不少包含多少个字符的程序吗?不可能吧?
在很多时候,需要存储的数据量到底有多大,在事先往往是一个未知数,要想处理好这类情况,就需要在C++程序里使用动态内存。
动态内存支持程序猿创建和使用种种能够根据具体需要扩大和缩小的数据结构,它们只受限于计算机的硬件内存总量和系统特殊约束。
接下来,我们将学到如何以这种灵活的方式与内存打交道。
静态内存就是我们此前一直在使用的东西:变量(包括指针变量)、固定长度的数组、某给定类的对象。我们可以在程序代码里通过它们的名字
或者地址来访问和使用它们。
使用静态内存的最大弊端是,你不得不在编写程序时为有关变量分配一块尽可能大的内存(以防不够存放数据)。一旦程序开始运行,不管实际情况如何,那个变量都将占用那么多的内存,没有任何办法能改变静态内存的大小。
动态内存由一些没有名字、只有地址的内存块构成,那些内存块是在程序运行期间动态分配的。
它们来自一个由标准C++库替你管理的“大池子“(装B术语称之为"内存池”)。从内存池申请一些内存需要用new语句,它将根据你提供的数据类型分配一块大小适当的内存。你不必担心内存块的尺寸问题,编译器能够记住每一种数据类型的单位长度并迅速计算出需要分配多少个字节。如果有足够的可用内存能满足你的申请·new语句将返回新分配地址块的起始地址。
如果没有足够的可用内存空间?
那么new语句将抛出std:bad_aloc异常!注意在用完内存块之后·应该用delete 语句把它还给内存池。另外作为一种附加的保险措施,在释放了
内存块之后还应该把与之关联的指针设置为NULL。
图说编程:
int * i =new int;
delete i;
i=NULL;
有一个特殊的地址值叫做NULL指针。当把一个指针变量设置为NULL时·它的含义是那个指针将不再指向任何东西:
int x;
x=NULL; //x这时候啥都不指向
我们无法通过一个被设置为NULL的指针去访问数据。
事实上,试图对一个NULL指针进行解引用将在运行时被坚持到并将导致程序中止执行。所以在用delete 释放内存后,指针会保留一个毫无意义的地址·我们要将指针变量赋值为NULL。
注意,静态内存这个术语与C++保留字static 没有任何关系。静态内存意思是指内存块的长度在程序编译时被设定为一个固定的值,而这个值在程序运行时是无法改变的。
new语句返回的内存块很可能充满“垃圾“数据。所以我们通常先往里边写一些东西覆盖,再访问它们,或者在类直接写一个构造器来初始化。
在使用动态内存时,最重要的原则是每一条new语句都必须有一条与之配对的delete 语句,没有配对的delete 语句或者有两个配对的delete 语句都属于编程漏洞。尤其前者,将导致内存泄露。
为对象分配内存和为各种基本数据类型(int,char,float…)分配内存在做法上完全一样。
-用new向内存池申请内存
-用delete 来释放内存
这个概念其实我们老早前就给大家演示了:在虚函数那一讲中有木有?!
我们再用一个栗子给大家巩固巩固之前的知识:
#include<iostream>
#include<string>
class Company
{
public:
Company (std::string thename);
virtual void printinfo();
protected:
std::string name;
};
class Techcompany : public Company
{
public:
Techcompany (std::string thename,std::string product);
virtual void printinfo();
protected:
std::string product;
};
Company ::Company (std::string thename)
{
name=thename;
}
void Company::printinfo()
{
std::cout<<"这个公司的名字是:"<<name<<"\n";
}
Techcompany::Techcompany (std::string thename,std::string product):Company(thename)
{
this->product=product;
}
void Techcompany::printinfo()
{
std::cout<<name<< "生产了大量的" <<product<< "这款产品" << "\n";
}
int main()
{
Company *company = new Company("apple");
company -> printinfo();
delete company;
company=NULL;
company=new Techcompany("apple","iphone");
company ->printinfo();
delete company;
company=NULL;
return 0;
}
搞对象的时候·千万不要忘记把方法声明为虚方法,如仍有疑问请回顾——虚方法。
在重新使用某个指针之前千万不要忘记调用delete 语句,如果不这样做,那个指针将得到一个新内存块的地址,而程序将永远也无法释放原先那个内存块。因为它的地址已经被覆盖掉了。
请记住delete 语句只释放给定指针变量正指向的内存块,不影响这个指针。在执行delete 语句之后,那个内存块被释放了,但指针变量还依然健在哦。
动态数组
虽然,前边我们讲过的用new给基本类型和对象在运行时分配内存。但它们的尺寸在编译时就已经确定下来-----因为我们为之申请内存的数据类型在程序里有明确的定义,有明确的单位长度!!
可是,总有些时候,必须要等到程序运行时才能确定需要申请多少内存,甚至还需要根据程序的运行情况追加申请更多的内存。
从某种意义上讲,这样的内存管理才是真正的动态!!! 这一讲中,我们将带大家编写一个程序:为一个整数型数组分配内存,实现动态数组。
给大家复习和进一步讨论下数组和指针的关系。在即将编写的栗子程序里有一个数组,它的长度在编写这个程序时是未知的,这意味着无法在定义这个数组时在方括号里给出一个准确的数字。
如何解决这个问题呢?
不知道鱼油们在脑海里会将数组和什么挂钩呢?
嗯,没错,是指针!
数组名和下标操作符[]的组合可以被替换成一个指向该数组的基地址的指针和对应的指针运算:
int a[20];
int * x= a;
指针变量x指向数组a的地址,a[0]和 *×都代表数组的第一个元素。
于是,根据指针运算原则a[1]等价于*(x+1)、a[2]等价于*(x+2),以此类推。
大家想想,我们把这个逻辑倒过来,会怎样?
嗯,反过来也成立,并且帮了我们一个大忙:把一个数组声明传递给new语句将使它返回一个该数组基类型的指针。
把数组下标操作符和该指针变量的名字搭配使用就可以像对待一个数组那样使用new语句为这个数组分配的内存块了。
例如:
int * x= new int[10];
//可以像对待一个数组那样使用指针变量X:
-×[1]=45:
-x[2]=8:
//当然·也可以用一个变量来保存该数组的元素个数:
int count 10:
int * x= new int[count];
删除一个动态数组要比删除其他动态数据类型稍微杂一点。
因为用来保存数组地址的变量只是一个简单的指针。所以需要明确地告诉编译器它应该删除一个数组。
具体的做法是在delete 保留字的后面加上一对方括号:delete [] x ;
这样的语法可能大家觉得挺奇葩,但确实挺好用,接下来的栗子给大家做下演示!!
#include<iostream>
#include<string>
int main()
{
unsigned int count =0;
std::cout<<"请输入数组元素的个数 : \n";
std::cin>>count;
int *x=new int [count];
for (int i = 0; i < count; i++)
{
x[i]=i;
}
for (int i = 0; i < count; i++)
{
std::cout<<"x["<<i<<"]的值是:"<<x[i]<<"\n";
}
return 0;
}
从函数或方法返回内存
动态内存的另一个常见用途是让函数申请并返回一个指向内存块的指针。掌握这个技巧很重要。尤其是在你打算使用由别人编写的库文件时。
如果不知道这个技巧,就只能让函数返回一个简单的标量值,如整型、浮点型或字符型。换句话说,它既不能返回一个以上的值,也不能返回数组之类比较复杂的数据结构。
可以这样说,只要你想让函数返回的东西不是二个简单的值,就需要学习本讲内容。
这个技巧的基本思路并不复杂:在函数里调用new语句为某种对象或某种基本数据类型分配一块内存,再把那块内存的地址返回给程序的主代
码,主代码将使用那块内存并在完成有关操作后立刻释放。
请看代码段:
#include<iostream>
int *newInt(int value);
int main()
{
int *x=newInt(20);
std::cout<<*x;
delete x;
x-NULL;
return 0;
}
int *newInt(int value)
{
int *myInt=new int;
*myInt=value;
return myInt;
}
为什么不应该让函数返回一个指向局部变量的指针
我们曾讨论过变量作用域的概念:函数或方法有它们自己的变量,这些变量只能在这个函数的内部使用,这些变量我们成为局部变量(local variable )。我们又知道如何利用指针在某个函数内部改变另一个函数的局部变量的值(例如传址调用)。这是绕开变量作用域的一种手段,在某些场合是非常必要的。
但必须明确地告诉大家:任何一个函数都不应该把它自己的局部变量的指针作为它的返回值!!因为局部变量在栈里,函数结束自动会释放。
如果你想让一个函数在不会留下任何隐患的情况下返回一个指针,那它只能是一个动态分配的内存块的基地址。
今天的知识点很容易让大家联想起C语言的指针函数,这里就借此之便给大家继续探讨下很容易混淆的两个概念:函数指针和指针函数
函数指针:
指向函数首地址的指针变量称为函数指针。
指针函数:
一个函数可以带回一个整型数据的值,字符类型值和实型类型的值,还可以带回指针类型的数据,
使其指向某个地址单元。
副本构造器
地球人都知道,我们可以把一个对象赋值给一个类型与之相同的变量。编译器将生成必要的代码把“源“对象各属性的值分别赋值给“目标“对象的对应成员。这种赋值行为称为逐位复制(bitwise copy)。
这种行为在绝大多数场合都没有问题,但如果某些成员变量是指针的话,问题就来了:对象成员进行位复制的结果是你将拥有两个一摸一样的实例,而这两个副本里的同名指针会指向相同的地址。
于是乎,当删除其中一个对象时,它包含的指针也将被删除,但万一此时另一个副本(对象)还在引用这个指针,就会出问题!
那聪明的人这时候可能会说“小甲鱼你hod住,如果我在第二个副本同时也删除指针,不就行了吗?”
好滴,我们姑且认为这样做逻辑上没有问题。但从实际情况看是不可能的。因为你想啊,我们的CPU本身就是逐条指令执行的,那么就总会有个先后顺序。当试图第二次释放同一块内存,就肯定会导致程序崩溃。
那么怎样才能解决这个问题呢?
在遇到问题的时候,人总是会想要是当初怎怎怎·现在就能咋咋咋酱紫。。。。这听起来像是在后悔说的话,但对于编程来说,绝对是有后悔药的!!要是程序员在当初进行对象"复制“时能够精确地表明应该复制些什么和如何赋值,那就理想了。
C++语言的发明者早就预料到这个问题,并提出了一个解决方案。
分析下面几行代码:
MyClass objl;
MyClass obj2;
obj2 = objl;
前两行代码很简明,它们创建出了两个myClass 类的实例obj1和obj2。第三行代码把obj1的值赋值给了obj2,这里就可能会埋下祸根!
那么·怎样才能截获这个赋值操作并告诉它应该如何处理那些指针呢?
答案是对操作符进行重载。没错,提供重载机制,事实上就是提供给我们后悔药!!!!
我们知道几乎所有的C++操作符都可以重载,而赋值操作符“=“恰好是“几乎所有“中的一个。
我们将重载“=“操作符,在其中对指针进行处理:
MyClass & operator (const MyClass &rhs);
上边的语句告诉我们这个方法所预期的输入参数应该是一个myClass类型的、不可改变的引用。
因为这里使用的参数是一个引用,所以编译器在传递输入参数时就不会再为它创建另外一个副本(否则可
能异致无限递归)。
又因为这里只需要读取这个输入参数,而不用改变它的值,所以我们用const把那个引用声明为一个常量确保万无一失。
返回一个引用,该引用指向一个MyClass 类的对象。如果看过我们待会实现的源码,可能会发觉这个没有必要。但是,这样确实是一个好习惯。另外的好处是方便我们把一组赋值语句串联起来·如:a=b=c;
查看下面的代码:
#include<iostream>
#include<string>
class Myclass
{
public:
Myclass(int *p);
~Myclass();
Myclass &operator=(const Myclass &rhs);
void print();
private:
int *ptr;
};
Myclass::Myclass(int *p)
{
ptr=p;
}
Myclass::~Myclass()
{
delete ptr;
}
//a=b
Myclass &Myclass::operator=(const Myclass &rhs)
{
if (this!=&rhs)
{
delete ptr;
ptr=new int;
*ptr=*rhs.ptr;
}
else
{
std::cout<<"赋值号两边为同个对象,不做处理!!!\n" ;//obj1=obj1
}
return *this;
}
void Myclass::print()
{
std::cout<<*ptr<<std::endl;
}
int main()
{
Myclass obj1(new int(1));
Myclass obj2(new int(2));
obj1.print();
obj2.print();
obj2=obj1;
obj1.print();
obj2.print();
return 0;
}
完了?没完!今天的主菜还没上呢。只对赋值操作符进行重载还不能完美地解决问题。正如刚才所说的,C+的发明者把解决方案弄得有点复杂。
改写下测试代码:
MyClass obj1;
MyClass obj2 = obj1;
这与刚才那三行的区别很细微,刚才是先创建两个对象,然后再把obj1赋值给obj2。现在是先创建一个实例obj1,然后再创建实例obj2的同时用obj1的值对它进行初始化。
虽然看起来好像一样,但编译器却生成完全不同的代码:编译器将在myClass 类里寻找一个副本构造器(copy constructor),如果找不到,它会自行创建一个。
即使我们对赋值操作符进行了重载,由编译器创建的副本构造器仍以”逐位复制“方式把obj1赋值给obj2。
换句话说,如果遇到上面这样的代码,即使已经在这个类里重载了赋值操作符,暗藏着隐患的“逐位复制”行为还是会发生。想要躲开这个隐患还需要亲自定义一个副本构造器,而不是让系统帮我们生成。
MyClass( const MyClass & rhs);
这个构造器需要一个固定不变(const)的myClass 类型的引用作为输入参数,就像赋值操作符那样。因为他是一个构造器,所以不需要返回类型,还记得吗?
修改后代码:
#include<iostream>
#include<string>
class Myclass
{
public:
Myclass(int *p);
Myclass(const Myclass &rhs);//添加了副本构造器
~Myclass();
Myclass &operator=(const Myclass &rhs);
void print();
private:
int *ptr;
};
Myclass::Myclass(int *p)
{
ptr=p;
}
Myclass::Myclass(const Myclass &rhs)//副本构造器
{
std::cout<<"进入副本构造器";
*this=rhs;
std::cout<<"离开副本构造器";
}
Myclass::~Myclass()
{
delete ptr;
}
//a=b
Myclass &Myclass::operator=(const Myclass &rhs)
{
if (this!=&rhs)
{
delete ptr;
ptr=new int;
*ptr=*rhs.ptr;
}
else
{
std::cout<<"赋值号两边为同个对象,不做处理!!!\n" ;//obj1=obj1
}
return *this;
}
void Myclass::print()
{
std::cout<<*ptr<<std::endl;
}
int main()
{
Myclass obj1(new int(1));
Myclass obj2(new int(2));
obj2=obj1;
obj1.print();
obj2.print();
std::cout<<"-------------------------------------\n";
Myclass obj3(new int(3));
Myclass obj4=obj3;
obj3.print();
obj4.print();
std::cout<<"-------------------------------------\n";
Myclass obj5(new int(5));
obj5=obj5;
obj5.print();
return 0;
}
静态对象强制类型转换
样例:
#include<iostream>
#include<string>
class Company
{
public:
Company (std::string thename,std::string product);
virtual void printinfo();
protected:
std::string name;
std::string product;
};
class Techcompany : public Company
{
public:
Techcompany (std::string thename,std::string product);
virtual void printinfo();
};
Company ::Company (std::string thename,std::string product)
{
name=thename;
this->product=product;
}
void Company::printinfo()
{
std::cout<<"这个公司的名字是:"<<name<<"正在生产"<<product<<"\n";
}
Techcompany::Techcompany (std::string thename,std::string product):Company(thename,product)
{
}
void Techcompany::printinfo()
{
std::cout<<name<< "生产了大量的" <<product<< "这款产品" << "\n";
}
int main()
{
Company *company = new Techcompany("apple","iphone");
Techcompany *teccompany = (Techcompany *)company; //强制类型转换
teccompany->printinfo();
delete company;
//delete teccompany;//重复释放内存
company=NULL;
teccompany=NULL;
return 0;
}
我们用传统的强制类型转换实现:把所需要的指针类型放在一对圆括号之间,然后写出将被强制转换的地址值。
Company * company= new Company( " APPLE" , " Iphone");
TechCompany * tecCompany= (Techcompany *) company:
注意不能既删除company ,又删除tecCompany 。因为强制类型转换操作不会创建一个副本拷贝,它只是告诉编译器把有关变量解释为另一种类型组合形式,所以他们指向的是同一个地址。
动态对象强制类型转换
虽然刚刚那个栗子程序看起来很美。但它仍有一个问题没有解决:万一被强制转换的类型和目标类型结构完全不同,咋整?
编译器很笨的,它仍然将按照我们的代码行事。这样子的程序是相当危险的,随时可能崩溃以及被崩溃。
因为在类继承关系之间跳来转去(也就是对有关对象进行强制类型转换)在面向对象的程序里非常重要,所以C++程序员准备了几个新的强制类型转换”操作符(高级)。
在这里插入代码片
注:只要你喜欢,你仍可以在C++里继续使用C的强制转换操作符(像刚才的栗子),但表中的操作符还能进行必要的类型检查,因而能够改善程序的可靠性。动态强制类型转换的语法与刚刚我们学到的有很大不同,它看起来更像是一个函数调用:
Company* company= new Company( " APPLE" , " Iphone);
TechCompany * tecCompany= dynamic_ cast <TechCompany*>(company);
先在两个尖括号之间写出想要的指针类型,然后是将被转换的值写在括号中。
#include<iostream>
#include<string>
class Company
{
public:
Company (std::string thename,std::string product);
virtual void printinfo();
protected:
std::string name;
std::string product;
};
class Techcompany : public Company
{
public:
Techcompany (std::string thename,std::string product);
virtual void printinfo();
};
Company ::Company (std::string thename,std::string product)
{
name=thename;
this->product=product;
}
void Company::printinfo()
{
std::cout<<"这个公司的名字是:"<<name<<"正在生产"<<product<<"\n";
}
Techcompany::Techcompany (std::string thename,std::string product):Company(thename,product)
{
}
void Techcompany::printinfo()
{
std::cout<<name<< "生产了大量的" <<product<< "这款产品" << "\n";
}
int main()
{
Company *company = new Techcompany("apple","iphone");
Techcompany *teccompany = dynamic_cast<Techcompany *>(company); //强制类型转换
teccompany->printinfo();
delete company;
//delete teccompany;//重复释放内存
company=NULL;
teccompany=NULL;
return 0;
}
避免内存泄漏
这个话题就像古时候女人的裹脚布,又长又臭哈~
前面我们讲过,分配了一个内存块但忘记了释放它,这是一种严重的错误。这样的内存块将等到程序执行结束时才会被释放掉。
如果程序运行很长时间(例如在服务器上,注意不是所有的操作系统都想windows一样每天都要重启哈)并且在不停地申请新内存块,忘记释放那些已经不再有用的老内存块,迟早将会把内存消耗殆层,直接导致后边的new操作无法执行甚至是崩溃!!!
这样的编程漏洞我们称之为内存池漏(memory leak),因为它会像水池里的漏洞那样把内存池里的可用内存慢慢地消耗殆尽。
new语句所返回的地址是访问这个内存块的唯一线索·同时也是delete语句用来把这个内存块归还给内存池的唯一线索。
我们一起来看下面这个栗子:
int *x;
x= new int[1000];
delete[ ] x;
x= NULL;
这意味着如果这个地址值(保存在×里)丢失了,就会发生内存泄漏问题。
地址值会因为很多原因而丢失哦,比如因为一个指针变量被无意中改写,例如:
int *x;
x= new int[3000];
x= new int[4000];
delete[ ] x;
x= NULL;
大家看出来了吗??这是会导致内存泄漏的情况之一。
会导致内存泄露的另一种情况是用来保存内存块地址的指针变量作用域问题,例如:
void foo()
{
My Class *x ;
x= new MyClass();
}
当foo函数结束时,指针变量x将超出它的作用域,这意味着它将不复存在。它的值当然就会丢失。
有两种方法可上以用来堵住这样的漏洞:
第一个方法是在return语句之前的某个地方插入一条delete x 语句:
void foo()
{
My Class *x ;
x= new MyClass();
delete x;
X= NULL ;
return ;
}
第二个方法是让函数把内存块的地址返回给它的调用者:
MyClass * foo()
{
My Class *x ;
x= new MyClass();
return x;
}
变量都有一个作用域:规定了它们可以在程序的哪些部分使用。
这个作用域通常就是对它们做出声明和定义的函数的函数体,如main函数或某个子函数。
如果被定义在任何一个函数的外部,变量将拥有全局作用域,这意味着它们可以在整个程序中的所有函数里使用。
不过应该尽量避免使用全局变量,因为它们往往会让代码变得难以调试和容易出错!
动态内存不存在作用域的问题,一旦被分配,内存块就可以在程序的任何地方使用。因为动态内存没有作用域,所以必须由程序员来跟踪它们的使用情况,并在不再需要用到它们的时候把它们及时归还给系统。
这里需要特别注意的是,虽然动态分配的内存块没有作用域,但用来保存其地址的指针变量是受作用域影响的。
命名空间和模块化编程
接下来我们将重点讨论两个相互关联的简单概念:
第一个概念是模块化(modularization )
把程序划分成多个组成部分(即所谓的“模块”)这是通过把程序代码分散到多个文件里,等编序时再把那些文件重新组合在一起实现的。
第二个概念是命名空间(namespace )
这个概念相比起C语言是C++里新增加的东西。编写的程序越多、编写的程序越复杂,就越需要使用命名空间。
头文件
只用一个源代码文件来保存程序的全部代码是可行的,但那会给编辑修改工作带来诸多不便。
我们可以借助于C++的预编译器和编译器的能力把一个复杂的应用程序划分成多个不同的文件,而仍保持它在内容和功能上的完整。
C++预处理器的#include 指令提供了一种能够让编译器在编译主程序时把其他文件的内容包括进来的机制。
例如用这个指令来包括iostream 头文件我们已经用过很多次了。
头文件的基本用途是提供必要的函数声明和类声明。比如string 头文件就定义了字符串应该如何创建和使用。
头文件可以细分为系统头文件和自定义头文件。
顾名思义,系统头文件定义的都是系统级功能,正式因为有了它们,C+代码才可以在某种特定的系统上运行。
如果你想在你的程序使用这些功能·就必须把相应的头文体包括到你的程序里来。
系统头文件的另一个重要作用是保证C+代码的可移植性,确保同样的C++代码在不同的操作系统上做同样的事情。
例如为Mac定义的cout和为windows 定义的cout做的事情一样,但内部的具体实现不见得一样。
在#include指令里,系统头文件的文件名要放在尖括号里给出,这是告诉编译器:应该到“标准的”地点寻找这个文件:# include < stdio. h>
创建头文件
在#include 指令里,自定义头文件的文件名要放在双引号里给出:#include "xxxxx. h "头文件是一些以.h作为扩展名的标准文本文件。
一般情况下,都应该把自定义的头文件和其余的程序文件放在同一个子目录里。
或者在主程序目录下专门创建一个子文件夹来集中存放它们。你可以用头文件来保存程序的任何一段代码,如函数或类的声明,但一定不要用头文件来保存它们的实现!!!!
与标准的C++源代码文件相比,在头文件里应该使用更多的注释。
绝大多数头文件是通用型的,不隶属于任何特定的程序。所以至少把它的用途和用法描述清楚。
应该在注释里说明的内容包括:创建日期、文件用途、创建者姓名、最后一次修改日期、有什么限制和前提条件等等。
另外头文件里的每一个类和函数也应该有说明。
在上面写了这个程序并分离头文件出来:
//ration.h
//created by Lee
//这个头文件声明了有理数类(rational class)
//类里面对四则运算进行了重载,以实现分数运算。
#include<iostream> //因为使用了这个类的功能
class Rational
{
//有理数
public:
Rational(int num,int denom); //num是分子,denom是分母 构造函数
Rational operator+(Rational rhs ); // rhs===right hand side
Rational operator-(Rational rhs );
Rational operator*(Rational rhs );
Rational operator/(Rational rhs );
void print();//声明了一个打印的方法
private:
void normalize();//负责对分数简化处理
int numerator;//分子
int denominator;//分母
friend std::ostream& operator<<(std::ostream& os, Rational f);//改进重载左移操作符
};
虽说头文件可以用来保存任意代码片段,但典型的做法是只用它们来保存函数声明、用户自定义类型数据(结构和类)、模板和全局性的常量。
如果你有一个程序需要多次调用一个或一组函数,或是你有一个或一组函数需要在多个程序里调用,就应该把它们的声明拿出来放到一个头文件里。
头文件应该只包含最必要的代码,比如只声明一个类或只包含一组彼此相关的函数。
使用头文件
在创建了头文件之后,只要把它的文件名用双引号括起来写在如下所示的指令里就可以导入它:# include " fishc.h"
如果没有给出路径名,编译器将到当前子目录以及当前开发环境中的其他逻辑子目录里去寻找头文件。
为了消除这种猜测,在导入自己的头文件时可以使用相对路径。
如果头文件与立程序文件在同一个子目录里,则可以这么写:# include "./fishc. h "
如果头文件位于某个下级子目录里,那么以下级子目录的名字开头:# include " includes/ fishc. h "
最后,如果头文件位于某个与当前子目录平行的”兄弟“子目录里。则需要这么写:# include " …/ includes/ fishc. h "
请务必注意windows 通常使用反斜杠作为路径名里的分隔符。
创建实现文件
回到Rational 这个栗子·我们带大家来进一步实现模块化编程。
rational .h头文件包含Rational 类的声明,但不包含这个类的实现代码。
这种分割可能刚开始接触的朋友觉得有点奇怪,但在实践中非常普遍。
因为把接口(函数的原型)和实现(函数体的定义)分开是对代码进行模块化的基本原则之—。
头文件的重要性不仅体现在它们可以告诉编译器某个类、结构或函数将有着怎样的行为,还体现在它们可以把这些消息告诉给程序员。
作为苦逼程序猿一枚,你只需看到函数的声明就可以了解到你需要知道的一切:函数的名字,它的返回值类型和它的输入参数的类型和数量。
知道了这些东西,你就可以使用那个函数了,而根本用不着关心它到底是如何工作的。
编译器就不同了,它必须读取某个类或函数的实现代码。
作为一个通用原则,应该把声明放在一个头文件里,把实现代码放在一个cpp文件里。
C预处理器
刚才的示例程序还有一个小小的问题需要解决,就是rationcal .cpp和main. cpp文件都包含了rational .h头文件。这意味着rational .h类被声明了两次·这显然没有必要(如果它是一个结构,声明两次还将导致编译器报错呢~)
解决方案之一是把其中一个文件里的#include 删掉即可。这固然很容易可以解决的问题,但却会给今后留下麻烦。。。。。。
当然我们在这里提出是因为有更好的解决方案!!!
利用C++预处理器,我们可以让头文件只在这个类还没有被声明过的情况下才声明它。
预处理器的条件指令
以前的课程中,我们曾建议大家注释很多段代码的话用预处理的方式,比起/**/要效果好:
#if 0
//这里有代码
//这里有好多代码
//这里有好多好多代码
//这里有好多好多好多代码
#endif
例如:
# ifndef LOvE FISHC
# define LOvE FISHC
# endif
这看起来好像没什么用,但事实却并非如此。这段代码的含义是:如果LOVE_FISH还没有定义则定义之,看出这有什么作用了吗?
# ifndef LOvE FISHc
# define LOvE FISHC
class rational {…};
# endif
# endif
如果LOVE_FISH还没有定义·这里将发生两件事儿:定义一次LOVE_FISHC ,然后对Rational 类做出声明等操作。
这样一来,即使包含着这段代码的文件在某个项目里被导入了100次,Rational 类也只会被声明一次。因为在第一次之后LOVE_FISHCE 就有定义!
作为一种固定模式,这里使用的常量名通常与相应的文件名保持一致,把句点替换为下划线。于是rational.h文件将对应RATIONAL_H。
命名空间
随着程序变得越来越复杂,全局作用域里的东西会越来越多,尤其是在使用外部函数库时。
这可能会演变成一个问题:因为没有两样东西可以有同样的名字。
解决方案之一是给每个变量、函数和类等取一个独一无二的名字。但这可能很困难或很麻烦(因为随着一个程序代码量的逐步增加,一个变量名可能会变成像"CountofItemsInTheArray "酱紫才能不重复)。
这时候正是“命名空间”(namespace )的用武之地。
命名空间其实就是由用户定义的范围,同一个命名空间里的东西只要在这个命名空间有独一无二的名字就行了。
因此,如果某个程序有许多不同的头文件或已编译文件,它们又各自声明了许多的东西,命名空间可以为它们提供保护。
创建命名空间
创建命名空间的办法很简单,先写出关键字namespace 。再写出这个命名空间的名字,然后把这个命名空间里的东西全部括在一对花括号里就行了。如下所示:
namespace myNamespace
{
//全部东西
}
注意在最末尾不需要加上分号哦。
正如我们刚才讲过的那样,命名空间可以让你使用同一个标识符而不会导致冲突:
namespace author
{
std:: string person;
}
namespace programmer
{
std:: string person;
}
使用命名空间
如果某个东西是在命名空间里定义的,程序将不能立刻使用它。
这正是命名空间的全部意义所在:把东西放在它们自己的小盒子里。不让它们与可能有着相同名字的其他东西发生冲突。
想要访问在某个命名空间里定义的东西,有三种方法可供选择。
第一种方法我们已经用了很多遍了:
std::cout< < I love u!\n";
第二种方法是使用usng指令:
using namespace std;
执行这条语句后,在std命名空间里定义的所有东西就都可以使用,我们便可以像下面直接使用:
cout< < "I love u";
不过,把命名空间里的东西带到全局作用域里。跟我们使用命名空间的本意托违背!!!!!
所以,不建议在文件开头直接用using namespace XX 这种设计风格。
最后一种方法是用一个using指令只把你需要的特定命名从命名空间提取到全局作用域:
using std: : cout;
cout< < "I love u \n" ;
最后请务必注意:using指令的出现位置决定着从命名空间里提取出来的东西能在哪个作用域内使用。
如果你把它放在所有函数声明的前面,他将拥有全局性,如果你把它放在某个函数里,那么它将只在这个函数里以使用。
链接和作用域
前边我们已经开始创建由多个文件构成的项目。是时候再来讨论下更复杂的变量作用域了。简单的理解,变量的作用域就是你可以在什么范围内访问这个变量。
地球人都知道,一个在任何函数之前定义的变量可以在任何一个函数里使用(这是一个全局变量),而在某个函数里定义的变量只能在那一个函数里使用(这是一个局部变量)。
那么,当一个项目由多个文件构成时,变量的作用域也会受到一定的影响!
与作用域有关的另一个概念是链接。当你同时编译多个文件时:
每个源文件都被称为一个翻译单元(translation unit),在某一个翻译单元里定义的东西在另一个翻译单元里使用正是链接发挥作用的地方。
作用域丶链接和存储类是相互关联的概念。它们有许多共同的术语,只是观察和描述问题的角度不同罢了。
存储类
每个变量都有一个存储类,它决定着程序将把变量的值存储在计算机上的神马地方、如何存储,以及变量应该有着怎样的作用域。
默认的存储类是auto(自动),但你不会经常看到这个关键字,因为它是默认的,阴魂不散的!
自动变量存储在称为栈(stack))的临时内存里并有着最小的作用域,当程序执行到语句块或函数末尾的右花括号时,它们将被系统回收(栈回收),不复存在。
与auto不同的是static 。static变量在程序的生命期内将一直保有它的值而不会消亡,因为它们是存储在静态存储区,生命周期为从申请到程序退出(和全局量一样)。另外我们稍后就会提到的,一个static 变量可以有external 或internal 链接。
第三种存储类是extern ,它在有多个翻译单元时非常重要。这个关键字用来把另一个翻译单元里的某个变量声明为本翻译单元里的一个同名全局变量。
注意·编译器不会为extern变量分配内存。因为在其他地方已经为它分配过内存。
用extern 关键字相当于告诉编译器:“请相信我,我发誓我知道这个变量在其他翻译单元里肯定存在,它只是没在这个文件里声明而已!”
还有一个存储类是register ,它要求编译器把一个变量存储在CPU的寄存器里。但有着与自动变量相同的作用。
register变量存储速度最快,但有些编译器可能不允许使用这类变量。
变量的链接和作用域
链接是一个比较深奥的概念,所以我们尽可能以浅显的文字来解释它。
在使用编译器建议程序时,它实际上是由3个步骤构成
1.执行预处理器指令:
2.把.cpp文件编译成.o文件:
3.把.o文件链接成一个可执行文件。
如今的编译器都是一次完成所有的处理。所以你看不到各个步骤(学习Win32、64汇编的童鞋懂~)
步骤一前边我们已经讨论过:执行预处理指令,例如把#include指令替换为相应的头文件里的代码。总的效果是头文件里的代码就像从一开始就在.cpp文件里似的。
步骤二是我们司空见惯的事情:把C++代码转换为一个编译目标文件。在这一步骤里,编译器将为文件里的变量分配必要的内存并进行各种错误检查。
如果只有一个C++源文件。步骤三通常只是增加一些标准库代码和生成一个可执行文件。
但当你同时编译多个源文件来生成一个可执行文件的时候,在编译好每一个组件之后,编译器还需要把它们链接在一起才能生成最终的可执行文件。
当一个编译好的对象(即翻译单元)引用一个可能不存在于另一个翻译单元里的东西时,潜在的混乱就开始出现了。
链接分为三种情况,凡是有名字的东西(函数、类、常量、变量、模板、命名空间,等等)必然属于其中之一:外连接(external )、内链接(internal )和无链接(none)。
外链接的意思是每个翻译单元都可以访问这个东西(前提是只要它知道有这么个东西存在)。
普通的函数、变量、模板和命名空间都有外链接。
就像main.cpp以使用rational.cpp文件里定义的类和函数一样,其实我们一直在使用,只是今天我们来一次总结。
说到变量,你可以这样试一试:
// this.cpp
int i1=1;
// that. cpp
int i2= i1;
不用试了,一看就有问题·对不对?
在编译that.cpp 文件时,编译器并不知道变量i1的存在。为了解决这个问题,我们可以在that.cpp里使围extern关键字去访问第一个翻译单元的变量。
// this.cpp
int i1= 1;
// that. cpp
extern int i1;
int i2 = i1;
内链接的含义是:在某个翻译单元里定义的东西只能在翻译单元里使用,在任何函数以外定义的静态变量都有内链接:
// this.cpp
static int d= 8;
// that.cpp
static int d =9;
这两个文件各有一个同名的变量,但它们是毫不相干的两样东西。
最后,在函数里定义的变量只存在于该函数的内部,根本没有任何链接(none)。
例子
由that.cpp this.cpp header.h构成
//header.h file
#ifndef HEADER_H
#define HEADER_H
unsigned long returnFactorial(unsigned short num);
// unsigned short headernum=5;
static const unsigned short headernum=5;
#endif
// that.cpp file
#include"header.h"
unsigned short thatnum = 8;
bool printme = true;
unsigned long returnFactorial(unsigned short num)
{
unsigned long sum = 1;
for (int i = 0; i < num; i++)
{
sum*=i;
}
if (printme)
{
return sum;
}
else
{
return ;
}
}
// this.cpp file
#include"header.h"
#include<iostream>
extern unsigned short thatnum; //使用了extern外链接
static bool printme = false;
int main()
{
unsigned short thisnum = 10;
std::cout<<thisnum<<"! is equal to "<<returnFactorial(thisnum)<<"\n";
std::cout<<thatnum<<"! is equal to "<<returnFactorial(thatnum)<<"\n";
std::cout<<headernum<<"! is equal to "<<returnFactorial(headernum)<<"\n";
if (printme)
{
std::cout<<"ohhhhh, yeah ";
}
return 0;
}
基本的模板语法
到目前为止,我们已经介绍了两种C++程序设计范型:
按照面向过程式范型把程序划分成不同的函数
按照面向对象式范型把代码和数据组织成各种各样的类并建立类之间的继承关系。
在这一讲,将给大家介绍另一种范型:泛型编程!
范型编程技术支持程序员创建函数和类的蓝图,即模板(template),而不是具体的函数和类。
这些模板可以没有任何类型:它们可以处理的数据并不仅限于某种特定的数据类型。
当程序需要用到这些函数中的某一个时,编译器将根据模板即时生成一个能够对特定数据类型进行处理的代码版本。
泛型编程技术可以让程序员用一个解决方案解决多个问题。
接下来我们先来学习如何编写和使用自己的泛型代码,然后再跟大家介绍标准模板库(Standard Template Library, STL)。
在泛型编程技术里,我么仍然需要编写自己的函数和类,但不必限定它们所使用的数据类型。
只需要使用一个占位符(通常用字母T来表示)然后用这个占位符来编写函数。
当程序需要这段代码时,你提供数据类型,编译器将根据你的模板即时生成实用的代码。
简单的说,编译器把模板里的每一个T替换为所提供的数据类型。
是不是有点像那个啥?那个#Define 。。。。
我们回来说说STL库。STL库是泛型编程技术的经典之作,它包含了许多非常有用的数据类型和算法。
有着这玩意·我们以后装逼那成本敢情就低很多了,有木有!!
以下代码定义了一个名为foo()的函数模板:
template < class T>
void foo( T param)
{
// do something
}
这里有几件事值得注意:
第一行代码里,在尖括号里有一个class T 用来告诉编译器:字母T将在接下来的函数里代表一种不确定的数据类型。
关键字class并不意味着这个是类。这只是一种约定俗成的写法。
在告诉计算机T是一种类型之后,就可以像对待一种普通数据类型那样使用它了。
然后我们现在呢。先教大家使用模板编写大量的示例代码。
函数模板
举个栗子:
交换两个变量的值是一种几乎所有的程序都需要用到的基本操作。因为这种交换如此常见,所以把它编写为一个函数是个好主意。
void swap( int & a, int * &b)
{
int tmp = a;
a = b;
b = tmp;
}
简单分析下:
如果我们想用这个函数来交换两个double 类型的变量的值,我们应该怎么办?
没错,我们可以再增加一个swap(double & a ,double &b)函数,因为C++支持函数重载。
我们发觉,我们不得不为要交换的每一种数据类型反复编写同样的代码。
这正是函数模板大显身手的地方,你用不着为每一种数据类型分别编写一个函数,只要告诉编译你已经为此准备好了一个模板就行!!!!
这样子等你再使用swap()函数时,编译器将根据模板自动创建一个函数,该函数会使用正确的数据类型完成交换变量值的任务。
接下来我们一起来完成一个示例程序,同时我们将尝试创建一个这样的函数模板。
#include<iostream>
#include<string>
template<class T>
void swap(T &a, T &b)
{
T tmp = a;
a = b;
b = tmp;
}
int main()
{
int i1 = 1000 ;
int i2 = 2000;
std::cout<<"交换前,i1"<<i1<<",i2"<<i2<<"\n";
swap(i1,i2);
std::cout<<"交换后,i1"<<i1<<",i2"<<i2<<"\n";
std::string s1="james";
std::string s2="kobe";
std::cout<<"交换前,s1"<<s1<<",s2"<<s2<<"\n";
swap(s1,s2);
std::cout<<"交换后,s1"<<s1<<",s2"<<s2<<"\n";
return 0;
}
在创建模板时,还可以用template <typename T >来代替remplate <class T >,它们的含义是一样一样的。
注意template <class T>中的class并不意味着T只能是一个类。
再强调一次,不要把函数模板分成原型和实现两个部分。
如果编译器看不到模板的完整代码,它就无法正确地生成代码。所得到的出错信息从”不知所云”到“胡说八道”什么样都有。
为了明确地表明swap()是一个函数模板,还可以使用swap<int>(i1,i2)语法来调用这个函数。这将明确地告诉编译器它应该使用哪种类型。
如果某个函数对所有数据类型都将进行同样的处理,就应该把它编写为一个模板。
如果某个函数对不同的数据类型将进行不同的处理,就应该对它进行重载。
类模板
类模板与函数模板非常相似:同样是先由你编写一个类的模板,再由编译器在你第一次使用这个模板时生成实际代码。
template < class T>
class MyClass
{
MyClass();
void swap( T &a, T &b);
}
构造器的实现将是下面这样:
MyClass< T>: : MyClass()
{
//初始化操作
}
因为myClass 是一个类模板,所以不能只写出MyClass ::MyClass(),编译器需要你在这里给出一种与myClass()配合使用的数据类型。必须在尖括号里提供它。因为没有确定的数据类型可以提供.所以使用一个作为占位符即可。
接下来的示例程序将使这一切更加明朗。
我们即将编写一个基于模板的栈。
栈是实际编程过程中一种非常有用的数据结构,它是一种数据存储机制。
栈只提供两个函数:一个用来把数据压入栈的顶部,另一个用来从栈取出顶部元素(先进后出)。
栈原理示例:
#include<iostream>
#include<string>
template<class T>
class Stack
{
public:
Stack(unsigned int size = 100 );
~Stack();
void push(T value);
T pop();
private:
unsigned int size;
unsigned int sp; //占指针
T *data;
};
template <class T>
Stack<T>::Stack(unsigned int size)
{
this->size=size;
data=new T [size];
sp=0; //指向栈底
}
template <class T>
Stack<T>::~Stack()
{
delete [] data;
}
template <class T>
void Stack<T>::push(T value)
{
data[sp++]=value;
}
template <class T>
T Stack<T>::pop()
{
return data[--sp];
}
int main()
{
Stack<int> instack(100);
instack.push(1);
instack.push(2);
instack.push(3);
std::cout<<instack.pop()<<"\n";
std::cout<<instack.pop()<<"\n";
std::cout<<instack.pop()<<"\n";
return 0;
}
内联函数
内联即inline ,可能很多朋友虽然听过这个词儿但却不是太熟悉,因为平时用之甚少???
我们第一想到的是内联函数,引入内联函数的目的是为了解决程序中函数调用的效率问题。内联函数从源代码层看,有函数的结构。而在编译后,却不具备函数的性质。编译时,类似宏替换,使用函数体替换调用处的函数名。
一般在代码中用inline修饰,但能否形成内联函数,需要看编译器对该函数定义的具体处理。
举个栗子:
inline int add (int x, int y, int z)
{
return x+y+z;
}
在程序中,调用其函数时,该函数在编译时被替代,而不像一般函数那样是在运行时被调用。
可以看到,类模板和函数模板的创建过程几乎没有什么区别。
“把相关代码放在一起”这条重要规则同样适用于类模板,不管是什么模板,编译器都必须看到全部的代码才能为一种给定的类型创建出一个新的实现来。
在创建类模板时,避免类声明和类定义相分离的一个好办法是使用内联方法。在类里,内联方法的基本含义是在声明该方法的同时,还对它进行定义。
class Person
{
Person(std::string name)
{
this-> name = name ;
}
//...
}
除了可以更好地帮助编译器处理类模板之外,使用内联方法还有一个很好的作用:可以让你少些字并让源代码的可读性变得更好。
好了,让我们一起来改善上节课的栈程序。
#include<iostream>
#include<string>
template<class T>
class Stack
{
public:
Stack(unsigned int size = 100 )
{
this->size=size;
data=new T [size];
sp=0; //指向栈底
}
~Stack()
{
delete [] data;
}
void push(T value)
{
data[sp++]=value;
}
T pop()
{
return data[--sp];
}
private:
unsigned int size;
unsigned int sp; //占指针
T *data;
};
int main()
{
Stack<int> instack(100);
instack.push(1);
instack.push(2);
instack.push(3);
std::cout<<instack.pop()<<"\n";
std::cout<<instack.pop()<<"\n";
std::cout<<instack.pop()<<"\n";
return 0;
}
如果你打算在自己的程序里使用Stack模板。一定要给它增加一个副本构造器和一个赋值操作符重载,就像我们之前做过的例子一样!!!
我们刚刚的代码还缺少必要的错误处理功能·例如在栈已满的时候调用push()方法,或者在栈为空的时候调用pop()方法,会导致程序运行出错。我们应该设法让栈在遇到这两种情况的时候抛出一个异常来处理。
C++并没有限制只能使用一个类型占位符,如果类模板需要一种以上的类型,根据具体情况多使用几个占位符即可。
template < class T, class U>
class MyClass
{
//......
}
在实例化时,我们只需要这么做:MyClass< int, float> myClass;
容器和算法
渐渐地我们发觉编写的每一个程序都或多或少地需要存储一些数据,而C++在这方面只提供了几种最基本的方法。
你可以创建局部或全局变量来保存单个值,可以使用数组来保存多个值。
今天的概念:能容纳两个或更多个值的数据结构,通常我们称为容器(container )。
这么说来,数组是C++唯一直接支持的容器,但数组并不适合用来解决所有的问题。
你打算编写一个简单的拼写检查程序,你会怎么做呢?
某朋友提议使用一个相当长的单词表,而检查某给定单词的拼写是否正确,就可以通过检查它是否列在那个单词表里来实现:如果单词在表里就说明它拼写正确。
好滴,环顾左右,我们可以直接利用的也只有数组这个容器可以存放一个好长的单词表。
但是我们好像为“只有耕不完的地,没有累不死的牛”提供了又一个佐证。
因为如果利用数组来实现这个单词表的话,我们将不得不遍历每一个数组元素并把它与给定单词进行比较。
我们不能说这种办法不能解决问题,只是这个解决方案的效率实在是太低下了。
计算机领域的科学家们在过去的几十年里投入了大量的精力来为不同类别的问题寻找最合适的数据结构。
就拿刚才提到的拼写检查程序来说吧,最适合用来解决这类问题的数据结构是散列表和二叉树。
这个话题我们会在今后的《数据结构和算法》这系列教程中详细讲解。
这两种数据结构以某种特殊的方式来存储数据,在那些数据里检查某个特定的元素是否存在的效率是最高的。
何谓容器我们已经知晓,那么我们如何来制作一个容器呢?
其实前两节课,已经带大家实现一种新容器。猜的没错,基于模板的Stack类就是一种新容器。
既然知道如何创建和使用模板,朋友们其实大可以抄起一本讨论数据结构的书去尝试实现一些自己的容器。
现在的C++程序员用不着那么辛苦了,我们可以坐享其成。
在C++标准库里有许多现成的容器,它们都经过了老一辈精心的设计和测试,可以直接拿来就用。
这节课,我们也是来教大家如何使用。解决一个问题,找到最合适的容器只是编程工作的一部分。还需要一些适当的函数(算法)来处理这个容器里的数据才能实现最优效率。
哈哈,这方面同样有许多“标准的”功能是你经常会用到的,在《数据结构和算法》我们将详细讲解。
向量容器
数组这种数据结构最大的先天不足就是它受限于一个固定的长度。
在程序里用int my Array[40]这样的语句定义一个数组时,程序将遇到两个问题:
首先,你最多只能在那个变量里存储40个整型数据,万一你需要存储第41个数据,那么你就相当不走运了。
其次,不管程序是不是真的需要存储40个整型数据,编译器都会为它分配40个整型数据的空间。
像这样的问题用C语言解决起来往往很复杂。而C++提供的解决方案就高明得多了。
C++标准库提供的向量(vector )类型从根本上解决了数组先天不足的问题。
就像可以创建各种不同类型的数组一样,我们也可以创建各种不同类型的向量。
std::vector<type> vectorName;
这种语法相信应该不会再感到陌生了。
我们用不着对一个向量能容纳多少个元素做出限定。因为向量可以动态地随着你往它里面添加元素而无限增大(前提是有足够可用的内存)。
然后你还可以用它的size()方法查知某给定向量的当前长度(它当前包含的元素个数)。
定义一个向量后,我们可以用push_back()方法往它里边添加东西。
我们还可以用访问数组元素的语法来访问某给定向量里的各个元素。
综合上述几点我们一起来完成一个简单栗子vector.cpp!!!
#include<iostream>
#include<string>
#include<vector>
int main()
{
std::vector<std::string> names;
names.push_back("xiaolee");
names.push_back("xiaoxue");
for (int i = 0; i < names.size(); i++)
{
std::cout<<names[i]<<"\n";
}
return 0;
}
C++标准库是用C++编写程序的乐趣之一,它可以让你轻而易举地解决许多非常复杂的问题。我们甚至不必完全了解它的内部工作情况。
C++的类型检查功能非常强大,如果你试图把一个其他类型的值放到一个字符串向量里,编译器会立刻报错。
把一些元素放到一个向量里以后,就可以用赋值操作符来改变它们的值了,就像对待数组元素那样:names[0]="Jonny ";
迭代器
上节课我们的栗子虽然工作得很好,并也使用了一个标准的容器(向量容器),但它还是有个小问题。
就是在遍历向量里的各个元素时,我们仍把它视为一个C++数组来对待。
刚好我们的向量容器允许使用下表操作符来访问它的各个元素:name[x]
但是如果想改用另一种不提供此方法访问的容器(比如栈),我们就不得不对程序做很多修改才得以实现。
因为对容器里的各个元素进行遍历是一种十分常见的任务,所以应该有一种标准的方式来做这件事,对吧?
C++标准库提供的各种迭代器(iterator )就是这么来的。
迭代器是一种功能非常有限却非常实用的函数,提供一些基本操作符:*、++、==、!=、= 。
迭代器是个所谓的智能指针,具有遍历复杂数据结构的能力。
因为迭代器的功能是如此的基本·所以标准库里的每一种容器都支持。
通过使用迭代器,当在程序里改用另一种容器的时候就用不着修改那么多的代码了。
每一种容器都必须提供自己的迭代器,事实上每种容器都将其迭代器以嵌套的方式定义于内部。
因此各种迭代器的接口相同,型号却不同,这就是所谓泛型程序设计的概念:所有操作行为都使用相同接口,虽然它们的具体实现不同。
修改vector .cpp,使用迭代器。
#include<iostream>
#include<string>
#include<vector>
int main()
{
std::vector<std::string> names;
names.push_back("xiaolee");
names.push_back("xiaoxue");
std::vector<std::string>::iterator iter = names.begin();
while (iter!=names.end())
{
std::cout<<*iter<<"\n";
++iter;
}
return 0;
}
算法
回顾我们刚才对向量示例程序的修改,别的先不说。用来遍历向量元素的循环比原来复杂了许多,可代码的行为还是老样子。
迭代器的真正价值体现在它们可以和所有的容器配合使用,而使用迭代器去访问容器元素的算法可以和任何一种容器配合使用。
小甲鱼将带大家再修改一下这个栗子,从而演示算法的威力:在输出该容器里的元素之前,先按字母顺序对它们进行排序。
对数据进行排序对人类来说好像只是个简单的问题,但要想高效率地完成这个任务,其实并没有想象中那么简单。
人们已经对排序问题总结出许多不同的算法:冒泡排序、堆排序、快速排序等等。
每种算法都有它们的优点和缺点,而只有那些最简单的算法才比较容易以我们个人之力实现。
所幸的是C++标准库包含着一个经过全面优化的排序算法,它的处理速度也非常理想。
要想使用这个算法,只需先把algorithm 文件包含到源文件里:#include <algorithm>
然后再像下面这样调用sort()方法就可以了:
std::sort( beginIterator, endIterator);
例子演示:
#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
int main()
{
std::vector<std::string> names;
names.push_back("aiaolee");
names.push_back("ciaoxue");
names.push_back("viaoxue");
names.push_back("eiaoxue");
names.push_back("oiaoxue");
names.push_back("piaoxue");
names.push_back("xiaoxue");
std::sort(names.begin(),names.end());
std::vector<std::string>::iterator iter = names.begin();
while (iter!=names.end())
{
std::cout<<*iter<<"\n";
++iter;
}
return 0;
}
转战数据结构和算法