C/C++
- C:结构化和模块化高级编程语言,面向过程编程,在大规模、复杂的程序中表现吃力;
- C++:从C发展而来,向下兼容C的所有内容,并增加面向对象编程机制,提高大规模程序开发效率;
- 编译和连接:
- 编译:利用编译器“complier”把源程序(后缀名
.cpp
)逐一翻译成二进制形式的后缀名为.obj
的“目标程序”(object program),编译的作用是对源程序进行语法、词法检查; - 连接:利用连接器“linker”将所有编译好的目标程序和系统库文件、系统信息连接起来,最终生成可执行的后缀名为
exe
的二进制文件;
- 编译:利用编译器“complier”把源程序(后缀名
C++的输入/输出流
cin
和cout
:C++的输入输出通过调用输入输出流库中的流对象cin
和cout
实现,相当于C中的scanf
和printf
,但注意,cin
和cout
不是C++内置语句,需要引入头文件iostream
;;- 流提取运算符
>>
和流插入运算符<<
:>>
用于从输入设备的输入流中提取数据,并送到计算机内存区中指定的位置,<<
用于将待输出的内容插入到输出流中,它形式上与位移运算符一模一样,事实上它是一种运算符重载;
- 语法:
cout
:cout<<表示式1<<表达式2<<...<<表达式n<<endl;
与C语言不同的是,C++的输出并不需要提供输出类型,计算机会自动判断输出数据的类型;
cin
:cin>>变量1>>变量2>>...>>变量n;
int main (void)
{
int a;
int b;
int m;
cin>>a>>b;
m=max(a,b);
cout<<"max="
<<m
<<'\n'; // m 已被定义为 int类型
return 0;
}
输出缓冲区
- 数据流:C++的输入/输出通过“流”的方式实现,“流”就是来自设备传给计算机内存或来自计算机内存传给设备的数据流,其由一系列字节组成,这些字节按进入“流”的顺序排序;
- 内存缓冲区:在定义数据流对象时,系统会在内存中开辟一段缓冲区,用于暂存输入输出流的数据,在执行
cout
语句时先把插入的数据按顺序存放到输出缓冲区中,直到缓冲区满或遇到endl
(或\n
、ends
、flush
)后才将缓冲区中的所有数据一起输出,并一次性清空缓冲区; cin
的数据输入方式:在连续输入数据时,应以回车或空格(分隔符)分开输入数据,否则会出现数据输入错误;endl
:它与回车换行\t\n
同理,另外endl
能保证数据立即刷新输出(即立即把消息显示在显示屏上),而其他(\n
、ends
、flush
)不能保证;
char c1,c2;
int a;
float b;
cin>>c1>>c2>>a>>b
输入数据时,可以用回车分隔:
1(回车)2(回车)34(回车)56.78(回车)
也可以用空格进行分隔:
1 2 34 56.78(回车)
标准输入/输出流中的格式控制符
使用格式控制符需要引入头文件iomanip
;
- 应用举例:
浮点数:
double a=123.456789012345
1.默认输出(精度6,输出6位有效数字)
cout<<a; //输出:123.456
2.精度9,输出9位有效数字
cout<<setprecision(9)
<<a; //输出:123.456789
3.以默认形式保留6位小数
cout<<setiosflags(ios::fixed); //输出123.456789
4.以默认形式保留8位小数
cout<<setiosflags(ios::fixed)
<<setprecision(8); //输出123.45678901
5.按指数形式输出,默认保留6位小数
cout<<setiosflags(ios::scientific) //输出1.234568e+02(第七位小数四舍五入)
整数:
int b=123456;
1.默认输出(按十进制整数形式输出)
cout<<b; //输出:123456
2.按十六进制整数形式输出
cout<<hex
<<b; //输出:1e240
3.按十六进制形式输出,字母为大写
cout<<setiosflags(ios::uppercase)
<<b; //输出:1E240
4.指定宽度为10,不够位数的在前面补空格
cout<<setw(10)
<<b
<<','
<<b; //输出:123456,123456
5.指定宽度为10,不够位数的在前面补“*”
cout<<setfill("*")
setw(10)
<<b; //输出:****123456
6.在正数前加“+”号
cout<<setiosflages(ios::showpos)
<<b; //输出:+123456
基本数据类型
布尔型_bool
又叫逻辑型数据;
在C语言中关系表达式值用1
和0
表示真和假,C++增加逻辑型(布尔型)数据,用于表示关系表达式的值;
- 逻辑常量:即
true
和false
,其数据类型为bool
型; - 逻辑变量:即用
bool
定义的变量,其取值只能是true
或false
;
在编译后,
true
和false
任然是1
和0
;
- 用输出语句输出时,输出结果仍然为1或0:
cout<<flag+true;
(flag
值为true
)
输出结果为2;
bool flag;
if(1+1==1){flag=true;}
else{flag=false;}
CPU寄存器变量_register
- 存储到CPU寄存器中的变量:一般情况下变量的值都存放在内存中,当程序中调用到该变量的值时,由CPU发出指令将内存中该变量的值传输到CPU的运算器中,若需要存储数据,再从CPU运算器传输到内存存放,有些变量存储频繁(如某变量在某函数中循环1000次,按常规,每次循环计算中变量值都要在内存和CPU之间进行传输),则存取变量值就要花费大量时间,在C++中,允许将局部变量的值存放在CPU的寄存器中,提高程序执行效率;
register int i=1;
- 寄存器变量只是给编译系统提一个建议:跟内置函数声明一样,声明寄存器变量只是给编译系统提一个建议,而不是指令性的,若寄存器变量配置不当,如该局部变量调用并不频繁,编译系统仍按常规把变量值存储到内存中;
字符串_string
- 字符串类型_
string
:在C语言中,字符串需要通过字符数组来存放,在对字符数组进行操作时,总是需要准确地计算字符数组的长度,在C++中提供了一种新的数据类型——字符串类型(string
),使用上和char
、int
一样,可直接用于定义字符串变量,从而使字符串独立于字符数组而存在; - 引入头文件
string
:数据类型string
并不是C++本身提供的基本数据类型,而是包含在头文件string
中,是C++标准库中声明的一个字符串类;
#include <string>
string string_1="hello world!",string_2,string_3; //定义字符串变量
1. 复制字符串
string_2=string_1;
2. 改变字符串中的某一字符
string_1[4]=a; //string_1的值变为:hella world!
3. 连接字符串
string_3=string_1+string_2;
4. 字符串运算
bool result;
if(string_1==string_2){result=true;}
else(string_1<string_2){result=false;}
- 字符串数组:即包含多个字符串的一个数组,数组的元素都是完整的字符串变量;
string name[5]=["string1","string2","string3","string4","string5"]
指针
const指针
- 限定指针变量指向的值:又叫指向常量的指针,使用关键字
const
,限定指针变量指向的值不能通过该指针修改;
int a=12,b=15;
const int *p=&a; //限定指针变量
*p=15 //非法
- 限定指针变量的指向:又叫常指针变量、常指针,使用关键字
const
,限定指针变量的指向不能改变;
char *const p="string_1"; //限定指针变量指向,它只能指向字符数组"string_1"
p="string_2"; //非法
- 指向常量的常指针:上述两者的结合,即指针变量的指向不能改变,也不能通过该指针修改其指向的值;
const int *const p=&a;
void类型指针
- 指向不确定的指针:void指针即指向尚不确定的指针,或叫指向空类型的指针,它无法访问任何一个具体的数据,它只提供一个指针指向的内存地址;
在C语言中,用
malloc
函数开辟动态内存空间,其函数返回值为该空间的内存地址,由于该控件尚未被使用,没有任何有用的数据,故返回一个void
型指针,显然,它必须转换为确定的数据类型数据时,才能访问其实际存在的数据;
- void型指针的赋值:可以把
非void
型的指针赋值给void
型指针,但不能直接把void
型指针赋值给非void
指针,必须先进行强制转换;
int a=3;
int *p1=&a;
char *p2="new";
void *p3; //定义void类型指针
//1. 注意:此时p3是(void *)型,若不给它转换为确定的数据类型,它不能被输出
//2. 将p1的值转换为 void *类型,再赋值给p3
p3=(void *)p1;
p3=p1; //把非void型指针赋值给void型指针,p3得到的只是变量a的纯内存地址值,而不指向a的内存地址
cout<<*(int *)p3
<<'\n'; // 将p3的值转换为(int *)型,再指向变量a
p2=(char *)p3; //将p3的值转换为(char *)型后赋值给p2
p2=p3; //直接把void型指针赋值给非void型指针,非法!
对象指针
- 对象指针:跟结构体指针类似,对象指针即指向对象或对象成员的指针;
- 指向对象的指针:在对象建立时,编译系统会为每个对象分配内存空间,以存放成员数据,对象的内存空间的起始地址就是该对象的指针;
- 指向对象成员的指针:同理,存放对象成员地址的指针变量即指向对象成员的指针;
1. 指向对象的指针
class Time
{
public:
int hour;
int minute;
int sec;
void get_time();
};
void Time::get_time()
{
cout<<hour<<":"
<<minute<<":"
<<sec<<'\n';
}
int main(void)
{
Time *p; //定义指向Time类的对象指针
Time t_1;
p=&t_1; //指针p指向对象t_1
//利用对象指针访问对象成员
(*p).hour=12;
p->hour=12;
(*p).get_time();
p->get_time();
}
2. 指向对象成员的指针
int *p; // 定义int类型指针
p=&t_1.hour; // 指针p指向对象t_1的成员hour
*p=11;
3. 指向对象成员函数的指针
3.1 指向普通函数的指针
void(*p)(); // 定义指向void型函数的指针(非指向对象成员函数)
p=function; // 将某函数入口地址(函数名)赋值给函数指针
(*p)(); // 调用函数
3.2 指向对象成员的指针
void(Time::*p)(); // 定义指向Time类中公共成员函数的指针
p=&Time::get_time(); // 指针指向Time类中的公共成员函数
this
指针
在每个成员函数中都包含一个指向本类对象的指针,叫this
指针,其值为当前被调用的成员函数所在的对象的起始地址;
在引用类成员变量时,以下语句是同理的:
(a.height)*(a.width)*(a.length)
(this->height)*(this->width)*(this->length)
引用
- 为变量起一个别名:“引用”(reference) 是C++的一个专有名词,其意义是给变量起一个别名,语法是
typename &reference= variable
,其中&
为引用说明符,而不是取地址符; - 引用本质是一个指针常量:引用本质上是一个指向不能改变的指针常量;
int a,c;
int &b=a; //声明b是a的引用,即b是a的别名
1. 非法
int &b=c; //非法,b已经是a的应用,不能再使之称为别的变量的引用
int &c=b; //非法,不能建立引用的引用
int &b=a[0] //非法,不能作为数组元素的引用
int *p=b; //非法,不能建立指向引用的指针
2. 合法
int *p=&b; //相当于把a的地址赋值给指针p
- 引用作为函数的形参:C++引入引用的最大目的,是把它作为函数的形参,因为引用本质上是指针常量,那么把引用作为函数的形参就相当于把指针作为函数的系形参,这样做的目的是提高代码编写、阅读效率;
#include <iostream>
using namespace std
int main(void)
{
void swap(int &,int &); //函数声明
int i=3,j=5;
swap(i,j); //实参是整型变量
cout<<"i="
<<i
<<",j="
<<j
<<'\n';
return 0;
}
void swap(int &a,int &b) //形参是引用
{
int temp;
temp=a;
a=b;
b=temp;
}
对象数组
- 对象数组:有多个同类对象组成的数组;
//假定已声明了Student类
Student stud[50]= //定义对象数组stud,含50个数组元素;
{
Student(1001,18,87), //调用第一个元素的构造函数,向他提供3个实参
Student(1002,19,76), //调用第二个元素的构造函数,向他提供3个实参
...
}
函数
内置函数
- 内置函数:将主调函数
main
所调用的代码字节嵌入到主调函数中,而不是将流程转出,目的是跳过函数调用的过程,提高程序的执行效率和减少程序执行时间,但由于把函数嵌入到主调函数中,增加了目标程序的长度,编译后,目标文件(.obj
)的长度也会加长,一般把规模小、调用频繁、实时性要求高的函数配置为内置函数;
如下图是正常的程序执行流程,在程序从主调函数跳转到a函数时,有一定的时间、空间占用,若把a函数设置为内置函数,则程序执行只有①和⑤;
-
内置函数只能包含简单的语句:内置函数不能包含复杂的控制语句,如循环和
switch
语句、函数递归等; -
内置函数只是给编译系统提一个建议:内置函数
inline
并不是指令性的,只是给编译系统提一个建议,若内置函数配置不当,编译系统仍按调用函数的方式执行程序; -
语法:直接在声明和定义函数时在函数首加关键字
inline
;
只在声明时加
inline
也可以;
inline int max(int,int); //声明内置函数
int main(void)
{
int i=10,j=20,m;
m=max(10,20);
cout<<"Max="
<<m
<<'\n'
return 0;
}
inline int max(int a,int b) //定义内置函数
{
int m;
if(b>a){m=b;}
else{m=a;}
return m;
}
函数重载
- 函数重载(function overloading):一般一个函数只能实现一种功能,通过函数重载,用同一个函数名实现多个类似的函数功能,函数重载允许形参数量、类型的不同,系统在调用函数时根据给定的数据取找到与之匹配的函数;
注:函数重载仍然是定义多个函数,只是其函数名相同,方便调用;
int main(void)
{
//在主调函数内声明重载函数
int max(int a,int b);
int max(int a,int b,int c);
double max(double a,double b);
int i_1,i_2,i_3,i_m,i_m2;
i_m=max(i_1,i_2);
cout<<"i_Max="
<<i_m
<<'\n';
i_m2=max(i_1,i_2,i_3)
cout<<"i_Max="
<<i_m2
<<'\n';
double d_1,d_2,d_m;
d_m=max(d_1,d_2);
cout<<"d_Max="
<<d_m
<<'\n';
int max(int a,int b)
{
int m;
if(b>a){m=b;}
else{m=a;}
return m;
}
int max(int a,int b,int c)
{
int m;
if(b>a){m=b;}
else if (c>a){m=c;}
{m=a;}
return m;
}
double max(double a,double b)
{
double m;
if(b>a){m=b;}
else{m=a;}
return m;
}
}
函数模板
- 函数模板(function template):建立一个通用函数,其函数类型和形参不具体定义而是用一个虚拟的类型来暂时代表,函数模板只需定义一次即可,在调用函数时系统会根据实参的类型来替换模板中虚拟的形参类型,它只适用于函数体相同、形参个数相同而类型不同的情况;
- 语法:
template <typename T>
或template <class T>
,typename
和class
同理,typename
是后期C++标准加入的,用于阐清其为类型名而不是类名,所以应首选typename
;
#include <iostream>
using namespace std;
template<typename T> //函数模板声明,其中T为虚拟类型参数
T max(T a,T,b) //函数模板声明,用T作虚拟的类型名,显然形参a、b的数据类型是未定义的
{
if(a>b){b=a;}
return b;
}
int main(void)
{
int i_1=158,i_2=-76,i;
double d_1=56.87,d_2=90.233,d;
i=max(i_1,i_2); //形参a、b在调用时被定义为int型
d=max(d_1,d_2); //形参a、b在调用时被定义为double型
cout<<"i_max:"
<<i
<<'\n'
cout<<"d_max:"
<<d
<<'\n'
return 0
}
带参数默认值的函数
- 带参数默认值的函数:在函数声明时,给形参指定一个默认值,若函数调用时该值没被赋值,则使用该默认值;
- 形参与实参对齐:实参(默认值)与形参自左向右依次结合,应把带默认值的形参放在形参列表的最右端,否则会出错,如
void i1(int a,int b,int c=0,char='a')
;
int max(int a,int b,int c=0); //在函数声明或定义中给形参赋默认值
cin>>a
>>b
>>c; //输入时还是输入3个实参,不然会发生编译错误
// 函数调用
m=max(1,2); //从两个数中找到最大数,c默认为0
m1=max(1,2,3);//形参c的默认值被覆盖
面向对象
-
基于过程与基于对象的程序设计区别:
- 基于过程:程序=算法+数据结构,在基于过程编程中,算法和数据结构是相互独立的,一组或多组算法处理多组数据,或一组数据被一组或多组算法处理;
- 基于对象:对象=算法+数据结构,程序=(对象+对象+…+对象)+消息,在基于对象编程中,一组算法和一组数据是封装成对象的,在同一个对象中,一个算法只处理一组数据,可以把对象想象成一个小程序,而总体程序由多个小程序组成,小程序之间通过信息交流;
-
对象(object):客观世界中任何一个事物都可以看成一个对象;
- 对象的静态特征和动态特征:
- 静态特征:几乎每个对象或者说一个集合内的对象都有的特征;
- 动态特征:某些对象特有的特征,又叫行为或功能;
- 对象的两个要素:属性(attribute)和行为(behavior),对象是由一组属性和一组行为组成的,属性即对象固有的特性,行为即对象根据外部消息而改变的功能;
- 对象的静态特征和动态特征:
-
消息(message):向对象发送一个消息,从外部控制对象的行为,也是对象之间的信息交流介质;
-
面向对象编程的4个主要特点:抽象、封装、继承、多态性;
- 抽象(abstraction)、类、对象:抽象即表示同一类事物集合的本质,如人是抽象的,张三是具体的人,国家是抽象的,中国是具体的国家;类(class) 是对象的抽象,如张三、李四是不同的对象,但他们都有手有脚,都有人类的属性,这些抽象起来的属性和行为即类;对象是类的具体实例(instance),如张三是人类的具体存在,中国是国家类的具体存在; 类是抽象的,不占用内存,对象是具体的,占用内存空间;
也可以说:类是对象的模板,对象是类类型的一个变量,也可以把类拟比成一种数据类型;
-
封装和信息隐蔽:一是将数据和操作代码封装到一个对象中,形成一个基本单位,各个对象之间相互独立,互不影响,二是将对象的某些部分屏蔽,对外只留下少量接口,一边与外界交流,,C++类对象中的函数名就是对象的对外接口,这种外界屏蔽的做法叫信息屏蔽;
-
继承和重用:面向对象的继承机制,即利用一个已有的类取创建另一个类,这就可以重用(reusability) 已有类的一部分属性和行为,节省编程工作量,如”马“是一个类,现在定义另一个类叫”白马“,只需从“马”这个类中添加颜色的属性即可,前者叫父类、基类,后者叫子类、派生类,子类还可以继承,如定义公白马;
-
多态性(polymorphism):由继承而产生的不同的子类,对同一消息回作出不同的响应,如若有几个相似但不完全相同的对象,向它们发送同一个消息,它们执行不同的操作,这就是多态性;
-
面向对象编程的基本原则:封装、信息屏蔽;
-
面向对象是C++的一种新的数据类型,类是这种数据类型的格式规范,它规范了成员数据和成员函数的格式,对象是类的实体,就像产品制造指导书和产品实体;
成员数据与成员函数
计算机程序(或者说类)由两个概念组成:数据和算法,就像要做一个布丁需要食材(数据)和配方(算法);
类的声明和对象的定义
- 类的声明:类的声明与C中结构体的声明类似;
- 成员访问限定符:
private(私有)
、public(公共)
和protected(被保护)
,用于声明类成员的访问属性;- 被声明为
private
的成员只能被本类中的成员函数调用,类外函数不能调用(友元类除外); - 被声明为
public
的成员可以被本类中的成员函数调用,也可以被类的作用域范围内的其他函数调用,是本类给外界开的接口; - 被声明为
protected
的成员不能被类外函数调用,但可被其子类(派生类)的成员函数调用;
- 被声明为
- 默认私有化:在类声明时,若不注明私有/共有,默认是私有
private
的;
-
类库:把常用的类封装成一个库;
-
结构体和类的对比:
//声明结构体
struct Student //结构体名
{
//结构体成员数据
int num;
char name[20];
char sex;
}; //不要忘记分号
//定义结构体变量
Student stud_1, stud_2;
//声明类
class Student //类名
{
//公有部分
public:
//类成员函数
void display()
{
cout<<"num:"
<<num
<<endl;
cout<<"name:"
<<name
<<endl;
cout<<"sex:"
<<sex
<<endl;
}
//私有部分
private:
//类成员数据,注意类不是实体,不能对类内的数据进行初始化赋值
int num;
char name[20];
char sex;
}; //不要忘记分号
//定义类对象
student stud_1, stud_2;
通过对比可知,C++类类型相当于带成员函数的结构体;
- 对象的定义:如上例程中已显示对象定义的一种方式,即先声明类类型,再定义对象,除此之外,还可以在声明类的同时定义对象、不填写类名下直接定义对象,这一点和结构体是非常类似的;
1. 先声明类类型,再定义对象,见上
2. 声明类的同时定义对象
class Student
{
//共有部分
public:
void display()
{
cout<<"num:"
<<num
<<endl;
cout<<"name:"
<<name
<<endl;
cout<<"sex:"
<<sex
<<endl;
}
//私有部分
private:
//类成员数据
int num;
char name[20];
char sex;
}stud_1,sutd_2;
3. 不出现类名,直接定义对象
class
{
//共有部分
public:
void display()
{
cout<<"num:"
<<num
<<'\n';
cout<<"name:"
<<name
<<'\n';
cout<<"sex:"
<<sex
<<'\n';
}
//私有部分
private:
//类成员数据
int num;
char name[20];
char sex;
}stud_1,sutd_2;
在类外定义成员函数
- 作用域限定符
::
(field qualifier):也叫作用域运算符,用于限定(qualifed)函数是属于哪个类的,若该限定符前没有任何类名,如:: display(){...}
,它等同于display(){...}
,即为全局函数;
把较长或较多的类成员函数定义在类之外,是一种较好的编程习惯,提高代码阅读性;
class Student
{
//共有部分
public:
void display(); //声明函数
//私有部分
private:
//类成员数据
int num;
char name[20];
char sex;
}
//在类外定义成员函数
void Student::display()
{
cout<<"num:"
<<num
<<endl;
cout<<"name:"
<<name
<<endl;
cout<<"sex:"
<<sex
<<endl;
}
Student stud_1,Stud_2;
内置类成员函数
- 内置类成员函数:与上面说的内置函数一样,可用
inline
建议编译系统把函数设置为内置函数,提高代码执行效率;
事实上,在类内定义函数若不包含循环等控制结构,编译系统会自动将其设置为内置函数,当然也可以手动设置,但在外部定义的类成员函数若要设置成内置函数,就必须把
inline
显式表示出来;
class Student
{
//共有部分
public:
inline void display(); //声明函数为内置函数,函数体在外部定义
inline void play() //类内定义的函数,`inline`写不写都一样,一般不写
{
...
}
//私有部分
private:
//类成员数据
...
}
//在类外定义成员函数,并设置成内置函数
inline void Student :: display()
{
...
}
Student stud_1,Stud_2;
访问对象的成员
-
三种方式:
- 通过对象名和成员运算符访问对象的成员;
- 通过指向对象的指针访问对象中的成员;
- 通过对象的引用访问对象的成员;
-
通过对象名和成员运算符访问对象的成员:利用成员运算符
.
用于指定成员所属的对象;
注意:对象中只有共有部分(
public
)的成员才能被外部访问;
//假定以下对象成员都是公有的(public)
stud_1.num=10; //访问对象成员数据
stud_1.display(); //访问对象成员函数
- 通过指向对象的指针访问对象中的成员:与指向结构体变量的指针同理,对象的也可被指针变量指向,用指针访问对象中的成员;
class Time
{
public:
int hour;
int minute;
};
Time t,*p' //定义对象t和指针变量p
p=&t;
cout<<"hour:"
<<p->hour //访问对象成员变量
<<'\n';
- 通过对象的引用访问对象的成员:已知“引用”是一个别名,对象也可以使用“引用”,可通过引用来访问对象中的成员,方法都是相同的;
Time t1; //定义对象
Time &t2=t1; //定义t2为t1的引用
cout<<"time:"
<<t2.hour
<<'\n';
对象成员数据的初始化
- 对象成员数据初始化:在定义对象时,可对对象中的成员变量进行初始化赋值,但注意不能对类中的成员变量赋值,因为类不是实体,没有内存空间;
class time
{
public:
hour;
minute;
sec;
};
Time t_1={14,56,30}; //定义对象并对其成员数据进行初始化赋值
构造函数
- 用构造函数实现成员数据的初始化: 构造函数(constructor) 是C++提供的用于处理对象成员数据初始化的一个类中的特殊成员函数,构造函数不需要用户调用,也不能被用户调用,它在建立对象之初自动执行,构造函数的名字必须与类名同名,没有返回值;
显然构造函数中的变量是要在类中先定义的,构造函数只是给其对象一个初始化的值;
注意:构造函数只有在对象建立之初被执行一次,而基于另一个对象建立的对象并不会调用类中的构造函数,而是直接赋值其基对象的数据;
- 默认构造函数:这种不带任何输入参数的构造函数,称为默认构造函数,每个类中,只允许出现一个默认构造函数;
声明类时可不定义构造函数,系统会在类声明时自动设置一个构造函数,在定义类对象时会自动调用该构造函数;
class Time
{
public:
Time() //构造函数
{
hour=12;
minute=12;
sec=12;
}
void set_time();
void show_time();
private:
//定义类成员数据
int hour;
int minute;
int sec;
};
void Time::set_time
{...}
void Time::show_time
{...}
int main(void)
{
//建立对象,同时自动调用构造函数t_1.Time(),所以该对象在被建立初,其成员数据就已经被初始化赋值过
Time t_1;
t_1.hour=18;
Time t_2=t_1; //建立对象t_2,并用对象t_1初始化t_2,此时t_2的数据完全复制对象t_1的数据而不是执行类中的构造函数
return 0;
}
- 带输入参数的构造函数:为了让不同的对象赋予不同的初始值,构造函数是允许带输入参数的,因为构造函数是不允许被调用的,它只能在建立对象时作为实参传给对象,然后再执行构造函数;
class Time
{
public:
Time(int,int,int); //声明构造函数
void set_time();
void show_time();
private:
int hour;
int minute;
int sec;
};
Time::Time(int h,int m,int s) //在类外定义带参数的构造函数
{
hour=h;
minute=m;
sec=s;
}
void Time::set_time
{...}
void Time::show_time
{...}
int main(void)
{
Time t_1(12,12,12); //建立对象,同时自动调用构造函数t_1.Time(),并传入初始化数据
return 0;
}
- 带参数初始化表的构造函数:在构造函数上更进一步,就有了更简便的参数初始化表;
1. 语法
类名::构造函数名([参数表])[:成员初始化列表]{构造函数体内容}
2. 例程
class Student
{
public:
Student(int n,char s,name[]):num(n),sex(s),name(name) //带参数初始化表的构造函数
{
strcpy(name.man); //构造函数体内容
}
private:
int num;
char sex;
char name[20];
};
Student stud_1(10101,'m',"zhang_san") //定义对象并初始化
可见,若带参数初始化表的构造函数的函数体内容为空,其相当于一个“在类外定义带参数的构造函数”
- 构造函数的重载:在同一个类中,可对构造函数进行重载,以便为对象提供不同的初始化方法,给用户选择;
class Box
{
public:
Box(); //声明默认构造函数
Box(int h,int w,int len):height(h),width(w),length(len){} //定义带参数初始化表的构造函数
private:
int height;
int width;
int length;
};
Box::Box()
{
height=10;
width=10;
length=10;
}
int main(void)
{
Box box_1; //定义对象,使用默认的构造函数
Box box_2(15,30,25);//定义对象使用带参数初始化表的构造函数
}
- 带默认值的构造函数:构造函数中成员数据的值既可通过实参传递,也可以指定为某些默认值,即如果用户不给出实参值,编译系统就使用默认值,这样可以减少一定的输入量;
class Box
{
public:
Box(int h=10,int w=10,int len=10); //声明带默认值的构造函数
Box(); //默认构造函数
Box(int h,int w,int len) //构造函数重载
private:
int height;
int width;
int length;
};
Box::Box(int h,int w,int len) //类外定义构造函数
{
height=h;
width=w;
length=len;
}
int main(void)
{
Box box_1; //定义对象,使用默认的构造函数,全部成员数据使用默认值
Box box_2(15);//定义对象,使用默认的构造函数,第一个成员数据height使用给定值,其他使用默认值
Box box_3(15,30);//定义对象,使用默认的构造函数,第一、二个成员数据使用给定值,第三个成员length数据使用默认值
Box box_4(15,30,20);//定义对象,使用默认的构造函数,全部成员数据使用给定值
// 显然对象box_4的定义会引起歧义,究竟使用重载的构造函数还是带默认值的构造函数?
}
当然以上构造函数也可以写成带参数初始化表的形式
一旦类中定义了带默认值的构造函数,就不应在定义重载构造函数,会出现歧义性;
析构函数
- 析构函数(destructor):跟构造函数一样,也是一种特殊的成员变量,其作用跟构造函数相反,语法也是在构造函数的函数名前加取反运算符
~
即成了析构函数,当对象的生命周期结束时,会自动执行析构函数,它的作用是在系统撤销对象占用的内存空间之前完成一些工作,清理工作,它没有任何输入参数、返回值,因为没有输入参数,自然也不能重载;
class Student
{
public:
Student(int,string,char); //声明构造函数
~Student(); //声明析构函数
void display(); //声明成员函数
private:
int num;
char name[10];
char sex;
};
Student::Student(int n,string name,char s) //定义构造函数
{
num=n;
name=name;
sex=s;
}
Student::~Student() //定义析构函数
{
cout<<"Destructor called."
<<"Program finished."
<<'\n';
}
int main(void)
{
Student stu_1(10010,"Zhang_san","m")
stud_1.display();
return 0;
}
- 构造函数与析构函数的调用顺序:跟栈的方式一样,先进者后出,后进者先出;
数据保护
常对象
定义对象时加关键字const
指定对象为常对象,注意常对像必须有初始值,常对象中所有数据的值不能被修改;
class Time
{
public:
int hour;
int minute;
int sec;
void get_time();
};
void Time::get_time()
{
cout<<hour<<":"
<<minute<<":"
<<sec<<'\n';
}
const Time t1(10,15,36); // 定义对象t1为常对象,其成员数据不可改变,其成员函数不可被调用
- 只有把类中的成员函数也声明为const类型,才能引用常对象的成员函数,此时常成员函数可以访问常对象中的成员数据,但任不可修改其数据值;
class Time
{
public:
int hour;
int minute;
int sec;
void get_time()const; // 将函数声明为const
};
- 修改常对象中某个成员数据的值 - 关键字
mutable
(可改变的):
class Time
{
public:
mutable int hour;
int minute;
int sec;
void get_time()const; // 将函数声明为const
};
常对象成员
将对象的成员(成员数据、成员函数)声明为const
型,此后只能通过构造函数的参数初始化表对常成员数据进行初始化,任何其他函数不能对其进行赋值修改;
class Time
{
public:
constint hour;
int minute;
int sec;
void get_time()const; // 将函数声明为const
};
Time::Time(int h):hour(h){} // 只能通过初始化表对常成员数据进行初始化
数据成员 | 非const 普通成员函数 | const 成员函数 |
---|---|---|
非const 普通成员数据 | 可引用,也可改变其值 | 可引用,不可改变其值 |
const 成员数据 | 可引用,不可改变其值 | 可引用,不可改变其值 |
const 对象 | 不允许 | 可引用,不可改变其值 |
对象的动态建立和释放
- 对象的静态建立和释放:无特殊注明,建立的对象都是静态的,其在建立完成后开始占用内存,直到函数调用结束,其内存才会被释放;
- 对象的动态建立和释放:提高内存空间利用率,在需要到对象时建立它,不需要时释放它,语法是用
new
建立一个对象,在不用时,用delete
释放其内存空间,其主要用于动态的数据结构,如链表等;
class Time
{
public:
int hour;
int minute;
int sec;
void get_time();
};
void Time::get_time()
{
cout<<hour<<":"
<<minute<<":"
<<sec<<'\n';
}
int main(void)
{
Time *p; // 定义一个指向Time类对象的指针
p=new Time(12,15,50); // 建立一个动态对象,把对象的起始地址赋值给指针
delete p; // 释放动态对象内存空间
}
同类对象的赋值
Student stu1,stu2; // 建立对象
stu2=stu1; // 把对象stu1中所有的成员数据赋值给对象stu2
对象克隆
Student stu2(stu1); // 根据已有的对象stu1克隆出一个新对象stu2
Student stu2=sut1; // 也可更直接地在建立对象的时候进行对象克隆
静态成员数据
以关键字static
定义的类成员数据,其数据内容供所有对象共用,其在内存中只占一份空间;
把类看作建学校的蓝图,成员数据看作学校内建筑,现以该蓝图建立一个大学城,为节约土地资源,
- 用
static
定义的建筑(如体育场、美术馆等)为该大学城内所有学校共有资源,其在大学城内只占用一份土地,每个学校都可以使用它,- 不用
static
定义的建筑为各学校私有的资源(如宿舍、食堂、图书馆等每个学校内都有的建筑),- 可见,每个学校都可将共用资源认为是自己拥有的资源,当这些共有资源发生变化时,将会影响到该大学城内的所有学校;
class Box
{
private:
static int h; // 定义h为静态成员数据
int w;
int l;
public:
int volume();
}
int Box::h=10; // 静态成员数据只能在类外进行初始化
静态成员函数
同理,类成员函数也可被static
定义为静态,其功能同静态成员数据,因为静态成员函数不属于某个对象,所以它没有this
指针;
静态成员函数主要用于访问静态数据成员,而不访问非静态成员,若要访问非静态成员,则须指明成员所属对象;
class Box
{
private:
static int h;
int w;
int l;
public:
static int volume(); // 定义静态成员函数
}
Box a(1,2,3); // 建立对象
cout<<h
<<endl; // 若h已被声明为静态,引用本类中的静态成员,合法,否则不合法
cout<<a.h
<<endl; // 指明非静态成员数据所属对象,合法引用
友元
- 友元(friend):友元是类中介于类外成员和类内成员关系之间的成员,若把类中公有成员
public
和私有成员private
分别比喻为主人(类)家里的客厅和卧室,类外成员于类内成员分别比喻为主人的访客和家庭成员,友元friend
就相当于主人的好朋友,它除可访问客厅外还允许访问卧室;
友元函数
在类中对某函数用friend
声明,该函数就成了该类的友元函数;
- 将普通函数声明为友元函数:
# include<iostream>
using namespace std;
class Time
{
private:
int hour;
int minute;
int sec;
public:
Time(int,int,int); // 声明构造函数
friend void displayTime(Time&); // 声明该公有函数为该类的友元函数
};
/* Time类构造函数 */
Time::Time(int h,int m,int s)
{
hour = h;
minute = m;
sec = s;
}
void displayTime(Time&t)
{
cout<<t.hour<<":"<<t.minute<<":"<<t.sec<<endl;
}
int main(void)
{
Time t1(10,13,56);
displayTime(t1);
return 0;
}
- 声明另一个类中的成员函数为本类友元函数:
# include<iostream>
using namespace std;
class Date;
class Time;
/* ---- 类 ---- */
class Time
{
private:
int hour;
int minute;
int sec;
public:
Time(int,int,int);
void displayTime(Date &); // 声明类成员函数,形参为Date类对象的引用
};
class Date
{
private:
int year;
int month;
int day;
public:
Date(int,int,int);
friend void Time::displayTime(Date &); // 声明Time中的函数为该类的友元函数
};
/* ---- 类构造函数 ---- */
Time::Time(int h,int m,int s)
{
hour = h;
minute = m;
sec = s;
}
Date::Date(int y,int m,int d)
{
year=y;
month=m;
day=d;
}
/* ---- 类成员函数 ---- */
/* 输入:Date类对象的引用 */
void Time::displayTime(Date &d)
{
cout<<"Current Date: "<<d.year<<":"<<d.month<<":" <<d.day<<endl; // 引用Date类中私有成员数据
cout<<"Current Time: "<<hour<<":"<<minute<<":"<<sec<<endl; // 引用Time类中私有成员数据
}
/* 主调函数 */
int main(void)
{
Date d1(2020,04,11);
Time t1(10,13,56);
t1.displayTime(d1);
return 0;
}
友元类
声明B类为A类的友元类,则B类中所有成员函数都是A类的友元函数,可访问A类的所有成员数据,相当于B类的全家人都可访问A类的卧室;
注意:
- 友元类是单向的;
- 友元类的关系不能传递,如A是B的友元类,B是C的友元,不代表A是C的友元类;
- 友元函数定义比友元类更安全;
类模板
把功能相同,数据类型不同的多个类写成一个类模板,实现一类多用,原理类似于函数模板;
# include<iostream>
using namespace std;
template<class dataType> // 声明一个模板,虚拟类型名为dataType
/* ---- 类 ---- */
class Compare
{
private:
dataType x,y;
public:
Compare(dataType a,dataType b){x=a;y=b;}
dataType max(){return(x>y)?x:y;}
dataType min(){return(x<y)?x:y;}
};
int main(void)
{
Compare<int> cmp1(3,7); // 定义对象
cout<<cmp1.max()<<" is the max value;"<<endl;
Compare<float> cmp2(3.0,7.5);
cout<<cmp2.max()<<" is the max value;"<<endl;
Compare<char> cmp3('a','A');
cout<<cmp3.max()<<" is the max value;"<<endl;
}
- 类模板中成员函数也可定义在类外部,但需在类模板外定义的成员函数前都加上模板声明;
# include<iostream>
using namespace std;
template<class dataType> // 模板声明,虚拟类型名为dataType
/* ---- 类 ---- */
class Compare
{
private:
dataType x,y;
public:
Compare(dataType a,dataType b){x=a;y=b;}
dataType max(); // 声明成员函数
dataType min();
};
template<class dataType> // 模板声明
dataType Compare<dataType>::min()
{
return(x<y)?x:y;
}
template<class dataType> // 模板声明
dataType Compare<dataType>::max()
{
return(x<y)?x:y;
}
int main()
{
Compare<int> cmp1(3,7); // 定义对象
cout<<cmp1.max()<<" is the max value;"<<endl;
return 0;
}
- 类模板的虚拟类型名可以为多个,在定义对象时需分别写出数据类型;
template<class dataType1,class dataType2>
className<int,float> objName(); // 定义对象
子类对象对其父类对象的赋值(赋值兼容)
与不同数据类型数据变量可相互赋值一样,不同对象之间也可以赋值,前面已讲过对象的赋值、克隆,对于公有继承的子类对象也可以对其父类对象进行赋值,赋值时子类对象的数据成员全部赋值给其父类对象,而子类新增的成员不进行赋值;
- 子类对象对父类对象的引用进行赋值或初始化:
A a1; // 定义父类A的对象a1
B b1; // 定义公有继承子类B的对象b1
A &r=a1; //定义父类A的对象引用r,并用其子类对象a1对其进行初始化,表示r是a1的引用(别名),r和a1共享一段存储单元
也可以用其子类对象对r进行初始化,把上面最后一行A &r=a1;改为
A &r=b1;
也可以对r重新赋值(保留A &r=a1;)
r=b1;
用类对象作为类成员数据(类的组合)
在类中可用对象作为成员数据,即子对象,它可以是本类的父类对象也可以是另一个类对象;
class Teacher
{
public:
...
private:
int num;
string name;
char sex;
};
class BirthDate
{
public:
...
private:
int year;
int month;
int day;
};
class Professor : public Teacher
{
public:
...
private:
BirthDate birthday; // 以BirthDate类对象作为成员数据
};
结果Professor类从Teacher类中得到num,name,sex等成员数据,从BirthDate类中得到year,month,day等成员数据
Professor prof1; // 定义类对象
有两函数:
void fun1(Teacher &); // 函数形参为Teacher类对象的引用
void fun2(BirthDate &); // 函数形参为BirthDate类对象的引用
在main函数中调用函数时
fun1(prof1); // 合法,实参为Teacher类的子类对象
fun2(prof1.birthday); // 合法,实参与形参类型对应
fun2(prof1); // 非法,实参是Professor类,形参与实参不匹配
继承与派生
- 继承:面向对象编程中的一个重要特点 ,提高了代码可重用率(software reusability),继承即在一个已有的类的基础上建立一个新的类,前者叫基类(base class)或父类(father class)后者叫派生类(derived class)或子类(son class),子类从父类中获得其特性,又可为自身新增另外的特性,如男人类从人类中继承人的所有特性又增加了雄性的特性,向下还可以有中国男人、美国男人等新类;
- 单继承:single inheritance,如下图 11.3,子类继承与单一的父类;
- 多重继承:multiple inheritance,如下图 11.4,子类继承于多个父类,如计算机专科继承于计算机专业和大专学历;
声明继承
- 继承方式:
- 公共继承-
public
:父类的公有成员和受保护成员在子类中保持原有访问属性,父类的私有成员仍为父类私有,这种继承方式让子类较好地保留父类的特征,除构造、析构函数外,子类保留了父类的所有成员,具有父类的所有功能; - 私有继承(缺省值) -
private
:父类的公有成员和受保护成员在子类中为私有成员,父类的私有成员仍为父类私有; - 受保护继承 -
protected
:父类的公有成员和受保护成员在子类中为受保护成员,父类的私有成员仍为父类私有;
- 公共继承-
受保护即使其不能被外界引用,但可悲子类成员引用;
- 注意:
- 子类接收父类的全部成员(构造函数、析构函数除外),此步骤不具选择性;
- 可通过继承方式可改变父类成员在子类中的访问属性;
- 可通过在子类中声明与父类成员同名的成员,进而覆盖取代它,若成员函数,为避免重载,应使函数名和参数表都相同;
语法:
class 子类名 : 继承方式 父类名
{
子类新增内容
};
例程:
# include<iostream>
#include<string>
using namespace std;
/* ---- 类 ---- */
class Human
{
public:
Human(int,string);
void displayInfo();
protected:
int age;
string add;
};
class Man : public Human
{
public:
// 构造函数
Man(string nation):Human(age,add){nation=nation;};
void displayInfo();
private:
string nation;
};
/* ---- 构造函数 ---- */
Human::Human(int age,string add)
{
age=age;
add=add;
}
/* ---- 成员函数 ---- */
void Human::displayInfo()
{
cout<<"age: "<<age<<endl;
cout<<"address: "<<add<<endl;
}
void Man::displayInfo()
{
cout<<"age: "<<age<<endl;
cout<<"address: "<<add<<endl;
cout<<"nation: "<<nation<<endl;
}
int main(void)
{
return 0;
}
子类成员的访问属性
情况 | 处理原则 |
---|---|
自己访问自己的成员 | 私有成员只能被同类成员函数访问,公用成员可被外界函数访问 |
父类成员函数访问子类成员 | 不允许 |
子类成员函数访问父类成员 | |
外界函数访问子类成员 | 只可访问公用成员 |
外界函数访问父类成员 | / |
子类的构造函数与析构函数
父类的构造函数与析构函数不能被子类继承,而子类新增的成员数据需要子类自己的构造函数进行初始化,实际上,在子类调用构造函数时,其父类的构造函数也同时被调用,完成了对子类从父类中继承的成员数据和其新增成员数据的初始化;
多重继承
- 声明多重继承:以下例程表示类D以公用继承方式继承类A, 以私有继承方式继承类B, 以受保护继承方式继承类C;
语法:
class D:public A,private B,protected C
{
...
};
- 多重继承子类的构造函数:类似于单继承子类的构造函数;
例程:
Man(string nation):Human(age,add),Animal(type)
{
...
};
多态性与虚函数
- 面向对象的多态性(Polymorphism):同一消息在不同对象接收时会产生不同的行为,即每个对象用自己的方式响应同一消息;
多态性极大程度地降低了编程工作负担,提高编程效率,就好比学校向外界发布开学消息,不同的对象接收到消息这一消息后会作出不同的响应,学生准备上学,家长准备学费,教师备课等,如果没有多态性,那么学校就要给不同的对象发不同的消息,布置不同的任务;
静态/动态多态性
面向对象编程的多态性的表现形式之一是让具有不同功能的函数赋予同一函数名,即函数重载;
- 静态多态性:通过重载实现,程序在编译时就知道调用函数的全部信息,系统就知道要调用哪个函数;
- 动态多态性:在程序运行过程中动态地确认调用哪个函数,通过**虚函数(virtual function)**实现;
其他
运算符重载
- 重载(overload):即“一名多用”;
动态分配、撤销内存空间_new
、delete
new
和delete
:在C语言中使用库函数malloc
、free
来分配和撤销内存空间,在C++中提供更为简便且功能更强的运算符new
和delete
来替代前者,虽然两者在C++中都可用,但优先推荐后者,因为后者是运算符,前者是函数,执行效率后者更高;
new int; //开辟一个存放整数的内存空间,返回一个指向该内存空间的地址(指针)
new int(100); //开辟一个存放整数的内存空间,并指定该整数的初始值为100,返回一个指向该内存空间的地址(指针)
new char[10]; //开辟一个存放包含10个数组元素的字符数组的内存空间,返回该字符数组的首元素地址;
float *p=new float(3.14159) //开辟一个存放单精度数的内存空间,并指定该数的初始值为3.14159,将返回的该内存控件的地址赋值给指针变量p
//撤销指针指向的之前开辟过的内存空间
delete p;
delete []p
C++标准库与头文件
C++包含C++标准库中的头文件时,与C语言有所不同,其在包含时不需加后缀名.h
,当然在C++中调用C语言标准库的头文件还是需要加后缀的,而且在C++中引入自编写的头文件时,建议加上后缀以易与C++标准库头文件进行区分;
#include <iostream>
#include <math.h> //C语言头文件
#include <cmath> //C++头文件