c++ 基础
目录
第一章 c++语言简介
第一节 C++语言的发展简史
贝尔实验室于1979年在c语言的基础上开发成功,C++兼容C语言,用C语言写的程序基本上可以不加修改地用于C++。
C++对C的"增强"表现在两个方法
(1)在原来面向过程的机制基础上,增加了C语言对类型的处理。
(2)增加了面向对象的机制。
C++语言是一种编译式的、通用的、大小写敏感的编程语言,完全支持面向对象的开发模式。
第二节 C++语言的特点
一、基本的输入/输出
#include <iostream>
using namespace std;
int main()
{
int a[10];
for (int i = 0; i < 10; i++)
{
cin >> a[i];
}
for (int i = 0; i < 10; i++)
{
cout << a[i] << " ";
}
cout << endl;
}
说明:
①标准输入:cin >> 变量1>>变量2>>...变量n;
②标准输出:cout<<表达式1<<表达式2<<...<<表达式n;
③使用标准输入cin及标准输出cout前,要在程序最前面包含:
#include <iostream> //包含头文件; 输入输出流
#using namespace std; // 使用命名空间
④换行操作: 用语句cout << endl; 或cout<<"\n";
⑤当连续从键盘读取想要的数据时,以空格、制表符<Tab>键或<Enter>键作为分隔符。如果第一个字符是空格、<Tab> 键或<Enter>,则cin会将其忽略并清除旧,继续读取下一个字符。
⑥用户自己定义的类型的数据,不能直接用">>"和"<<"进入输入/输出,必须对">>"和“<<”进行运算符重载后才可以使用.
二、头文件和命名空间
头文件
前面例题中 #include <iostream>
每一个C++程序由头文件和定义文件组成。头文件作为一种包含功能函数、数据接口声明的载体文件,主要用于保存程序的声明,而定义文件用于保存程序的实现。
头文件是用户应用程序和函数库之间的桥梁和纽带。编译时,编译器通过头文件找到对应的函数库,进而把已引用函数的实际内容导出来代替原有函数,然后再进行翻译。
常用的头文件:
-
标准输入输出流: <iostream>
-
标准文件流:<fstream>
-
标准字符串处理函数:<string>
-
标准数学函数: <cmath>
除了可以使用系统提供的头文件处,程序员可以自定义自己的头文件。
命名空间
一个大型的程序不同模块中出现的标识符之间有可能发生重名现象,这就会引发错误。C++中为了避免名子定义冲突,特别引入了“命名空间”的定义,即namespace。命名空间的作用是为了消除同名引起的岐义。
在程序中使用标准程序库中的标识符时,要写语句"using namespace std;"
三、强制类型转换运算符
当不同类型的量进行混合算术运算时,系统自动进行全理的类型转换,也可以强制转换类型。
-
将一种数据类型转换成另一种数据类型
static_cast <类型名>(表达式)
说明:static_cast -- 可以省略不写
oneint2 = static_cast<int>(oneDouble);
oneint2=int(oneDouble);
oneint2=(int)oneDouble; //圆括号
oneint2=oneDouble; //自动类型转换
例:
int b; float fa=30.45; b=static_cast<int>(fa); b=int(fa); b=(int)fa; //旧有强制转换的形式 b=fa; //自动类型转换 -
将常量指针转化成非常的指针,并且仍然指向原来的对象,或是将常量引用转换成非常量的引用,并且仍然指向原来的对象。
const_cast<类型名> (表达式)
例:
int a = 10, *q; const int ca=30; const int *p=&a; q=const_cast<int *>(p); //q=p *q=20; //变量a的值变为20,不能形成*p=20
四、函数参数的默认值
C++中,可以在声明函数时为形参指定的默认值。
#include <iostream>
using namespace std;
void func(int a = 11, int b = 22, int c = 33)
{
cout << " a=" << a << " b= " << b << " c=" << c;
cout << endl;
}
int main()
{
func();
func(55);
func(77, 99);
func(8, 88, 888);
return 0;
}
★c++语言规定,定义函数时,只能为函数最后面的连续若干个参数设置默认值,且在调用处也只能缺省后面的连续若干个实参。
例:判断以下函数声明正确与否 //注意不是定
void df1(int a=2, double b=3.0); //正确
void df2(int a, double b=3.0); //正确
void df3(int a=2, double b);
void f1(int a, int b=2, bint c=3); //正确
void f2(int a=1, int b, int c=3);
void f3(int a=1, int b=2, int c);
★C++语言规定,在函数调用处只能缺省后面的连续若干个实参,而且所有缺省的实参必须已经有默认值。
例:函数调用示例
函数声明: void func(int a, int b=2, int c=3)
函数调用:func(1, 22, 33);
func(); //错误
func(10, 20);
func(5, 9); //错误
★C++语言规定,指定默认值时不仅可以用常数,还可以用任何有定义的表达式作为参数的默认值。
例:假设给出如下的函数及变量声明:
int Max(int m, int n)
int a, b;
void func2(int x, int y=MAX(a, b), int z=a-b)
{......}
调用 func2(4); 等价于func2(4, MAX(a, b), a-b)
func2(4, 9); 等价于func2(4, 9, a-b);
func2(4, , 10); 错误
注意:函数参数的默认值可以写在声明函数的地方,也可以写在定义函数的地方,但不能在两个地方都写。函数声明表示有这么个函数了,函数定义就是具体实现了。
函数声明:
int fun(int a, int b);
函数定义:
int func(int a, int b)
{
int c;
c=a+b;
return c;
}
五、引用和函数参数的传递
1.引用:相当于给变量起了一个别名,别名的地址与引用变量的地址是一样的。程序中使用哪个 名字都是允许的。
“引用”的定义格式:
类型名 &引用名 = 同类弄的变量名;
例: int x=10;
int &a=x; //a就是x的一个引用,可以有多个引用
注意:① 对象在引用前必须先初始化
② 声明中符号 "&" 的位置无关紧要
如 int& a=x; int & a=x; int &a=x; 等效
常引用的格式:
const 类型名 &引用名 = 同类型的变量名;
例: int x=10;
int &a=x; //a就是x的一个普通引用
const int &b=x; //b就是x的一个常引用
a=20; //则20, b=20
x=30; //则a=30, b=30
b=40; //错误
注意: 不能通过常引用去修改其引用的变量的值。
-
引用在函数中使用
★ 引用作为函数的参数
函数调用时参数的传递有两种方式:传值和传引用,传引用是传递对象的首地址值,形参的改变就意味着实参的改变。
例:
#include <iostream> using namespace std; void SVal(int a, int b) { int tmp; tmp = a; a = b; b = tmp; cout << "在函数SVal()中:\t\ta=" << a << ", b=" << b << endl; return; } void SRef(int& a, int& b) //a b值互换 { int tmp; tmp = 1; a = b; b = tmp; cout << "在SRef()函数中:\t\ta=" << a << ", b=" << b << endl; return; } int main() { int a = 10, b = 20; cout << "数据交换前:\t\ta=" << a << ", b=" << b << endl << endl; SVal(a, b); cout << "调用SVal()后:\t\ta=" << a << ", b=" << b << endl << endl; a = 10; b = 20; SRef(a, b); cout << "调用SpRef()后: \t\ta=" << a << ", b=" << b << endl; return 0; }★引用作为函数返回值
返回引用的函数原型的格式如下:
数据类型 & 函数名(参数列表);
例:引用作为函数返回值
#include <iostream> using namespace std; int oneX = 10; int oneY = 20; int & refValue(int& x) //函数返回值是引用 { return x; } int main() { refValue(oneX) = 30; //返回值是引用的函数调用表达式,可以作为左值使用 cout << "oneX=" << oneX << endl; //输出oneX=30; refValue(oneY) = 40; cout << "oneY=" << oneY << endl; //输出 oneY=40; return 0; }
六、const与指针共同使用
当const与指针共同使用时,其书写的位置不同,语句含义也不同
1) 如果唯一的const位于符号*的左侧,表示指针所指数据是常量,数据不能通过本指针改变,但可以通过其他方式进行修改。指针本身是变量,可以指向其他的内存单元。
例: int a1=10; a2=20; *pa1=&a1;
*pa1=30; //正确, a1变成10
const int *pa2=&a2; //pa2所指的是常量,pa2是变量
*pa2=40; //错误,pa2所指的是常量,不能修改其值
pa2=&a1; //正确, pa2可以指向其他变量
2) 如果唯一的const位于符号*的右侧,表示指针本身是堂量,不能让该指针指向其他内存地址,指针所指的数据可以通过本指针进行修改。
例: int a1=10, a2=20;
int * const pa2=&a2; //指针变量pa2是常量
pa2=&a1; //错误
*pa2=40; //正确
3)在符号*的左右各有一个const时,表示指针和指针所指数据都是常量,即不能让指针指向其他地址,也不能通过指针修改所指向的内容。
例: int a1=10; a2=20;
const int * const pa1 = &a1; //数据和指针都是常量
pa1=&a2; //错误
*pa1=30; //错误
int const * const pa2=&a2; //数据和指针都是常量
pa2=&a1; //错误
*pa2=40; //错误
记住const的修饰规则:const修饰其左侧的内容; 如果const是本行的第一个标识符,则它修饰其右侧的内容。
const int *pa2=&a2; //pa2所指的是常量
int * const pa2=&a2; //指针变量pa2是常量
const int * const pa1=&a1; //数据和指针都是常量
int const * const pa2=&a2; //数据和指针都是常量
七、内联函数
对于需要频繁调用,且代码量少的函数,可以将其定义为内联函数。编译时,编译程序将整个函数体的代码复制到调用该函数的位置。
定义内联函数的格式如下:
inline 返回值类型 函数名(形参表)
{ 函数体 }
如果函数体中有循环语句和switch语句则通常不定义为内联函数。
八、函数的重载
函数重载:c++允许为同一个函数定义几个版本,从面使一个函数名具有多种功能,这称为函数重载。只要分别为不同参数编制相应的函数体,就可以实现各自的功能。
例: 函数重载的例子1
#include <iostream>
using namespace std;
int max(int, int); //声明2个整型参数的函数原型
int max(int, int, int); //声明3个整型的函数原型
int main()
{ cout<<max(36, 25)<<", " << max()<<endl; }
int max(int m1, int m2) //定义2个整型参数函数
{
return (m1 > m2)?m1:m2;
}
int max(int m1, int m2, int m3)
{
int t = max(m1, m2);
return max(t, m3);
}
例: 函数重载的例子2
#include <iostream>
using namespace std;
int bigger(int x, int y)
{
if (x > y)
{
return x;
}else
{
return y;
}
}
float bigger(float x, float y)
{
if (x > y)
{
return x;
}else
{
return y;
}
}
double bigger(double x, double y)
{
if(x>y)
{
return x;
}else
{
return y;
}
}
int main()
{
int x1 = 10, y1 = 20;
float xF = 30, yF = 40;
double xD = 50, yD = 60;
cout<<bigger(x1, y1)<<endl; //输出20
cout<<bigger(xF, yF)<<endl; //输出40
cout<<bigger(xD, yD)<<endl; //输出60
}
实现函数的重载必须满足下列条件之一:
-
参数表中对应的参数类型不同;
-
参数表中参数个数不同:
-
参数表中不同类型参数的次序不同。
注意:
1)两个函数的名字和参数表都是一样的,仅仅是返回值类型不同,则这两个函数不是重载的。
例: 错误的重载函数
float add(int ,float);
int add(int, float); //错误
2)函数的参数采用引用的,不能区分函数,则这两个函数不是重载的。
例: 错误的重载函数
void print(double);
void print(double &); //错误
3) 函数调用可能会引发二义性,不能采用函数重载。
例: 若定义了重载函数Sum().如下所示
int Sum(int a, int b, int c=0);
int Sum (int a, int b);
则函数调用语句: Sum(1, 2); //调用会产生二义性
4) 调用语句中的实参与函数形参的类型不完全匹配,存在赋值兼容的情况。此时,编译器也可以确定要调用的函数。
例: 调用函数时进行必要的类型提升
double bigger(double x, double y)
{
if(x>y)
{
return x;
}else
{
return y;
}
}
int xI=10, yI=20;
floatxF=30, yF=40;
double xD=50; yD=60;
cout<<bigger(xI, yF)<<endl; //int与float自动转换为double
九、指针和动态内存分配
-
指针: 即指针变量,该变量储存的是一个地址,是该指针所指对象的首地址。
int a=100, *pa=&a;
int s[10], ps=s; //指针ps指向数组s的首地址
2.动态内存分配
动态分配内存一般格式为:
指针名 = new 类型名; //分配
delete 指针名; //释放
当不再使用这个空间时,必须使用delete释放空间。若使用new运算符动态分配了一个数组,那么释放该数组时,语句如下: delete [] 指针。
例 演示使用new和delete的例子。
#include <iostream>
using namespace std;
int main(){
double* p; //声明double型指针
p = new double[3]; //分配3个double型数据的存储空间
for (int i = 0; i < 3; i++) //定义对象i的初值为0
{
cin >> *(p + i); //将输入数据存入指定地址
}
for (int j = 0; j < 3; j++)
{
cout << *(p + j) << " "; //将地址里的内容输出
}
delete[]p; //释放空间
}
十、用string对象来处理字符串
C语言用字符串数组来处理字符串,C++提供string数据类型来处理字符串。
1.声明string对象
例: string str1; //声明string对象str1, 值为空
string city="Beijing"; //声明string对象并初始化
string str2=city; //使用字符串变量进行初始化
char name[]="C++程序";
string str3 = name; //使用字符数组对string变量进行初始化
string citysf[] = {"Beijing", "Shanghai", "Tianjing"}; //声明string对象数组,每个数组元素都是字符串
说明:1) 使用string对象,必须#include <string>;
2) string对象储存的是字符串的首地址,非字符串本身; sizeof(string) 在32 位的Dev C++中是4,在64位的Dev C++中是8。
2.string对象的操作
◆ string对象可以使用cin和cout进行输入和输出
例:string s1, s2;
cin>>s1>>s2;
cout<<s1<<", H"<<s2<<endl;
◆ string对象之间可以互相赋值,也可以用字符串常量和字符数组的名字对string对象进行赋值。
例: string s1, s2="OK";
s1="China";
s2=s1; //赋值后s2的内容和s1相同
◆ string对象之间可以用">", ">=", "==",“<=”,"<","!="运算符进行比较。大小的判定标准是按字典顺序进行的、而且是大小写相关的。
例:bool b; //C++ 新增的bool类型,该类型只有两个取值1或0, 1表示"真", 0表示"假";
string s1="China"; s2="OK";
b=s1>s2; //变量b的值为0;
◆ 使用运算符"+"对字符串进行连接
例:string s1="China", s2="OK", s3=" ";
s1=s1+s3+s2; //连接后s1的内容“China OK”
3.string类中的常用成员函数
【例】字符串成员函数的使用
#include <iostring>
#include <string>
using namespace std;
int main()
{
string str; //未初始化, 空串
if (str.empty())
{
cout << "str is not NULL" << ", length=" << str.length() << endl;
}
else
{
cout << "str is not NULL" << endl;
}
str = str.append("abcdefg"); //字符串连接到当前字符串结尾处
cout << "str is" << str << ", size=" << str.size() << str.length() << endl;
cout << "length=" << str.length() << endl;
const char* p = str.c_str(); //p指向了字符串str
cout << "p = " << p << endl; //
cout << "find: " << str.find("de", 0) << endl;
cout << "find: " << str.find("de", 4) << endl;
string str1 = str.insert(4, "123"); //从str的第4位置插入"123"
cout << str1 << endl;
return 0;
}
第三节 C++语言的程序结构
◆C++程序以.cpp作为文件扩展名,文件中包含若干个类和若干个函数。
◆程序中有且仅有一个主函数main(), 程序从主函数的开始处执行,在主函数结束。
程序的结束通常是遇到了以下两种情形之一。
1)在主函数中遇到了return语句。
2)执行到主函数最后面的括号}。
◆主函数中可以调用程序中定义的其他函数,但其他函数不能调用主函数,其他函数直接可以相互调用。
◆c++程序中注释有以下两种形式。
1) 从/*开始,到*/结束, 如 /* …… */
2) 从//直到行尾, 如: //……
◆C++的格式和C一样,都很自由,一行可以写几条语句,但也要注意错落有致,增加可读性。
第二章 面向对象的基本概念
第一节 结构化程序设计
结构化程序设计的基本方法:采用自顶向下、逐步求精及模块化的思想,将复杂的大问题层层分解为许多 简单的小问题。
结构化程序设计的三种基本结构:顺序结构、选择结构、循环结构。
结构化程序设计的基本思想: 数据结构 + 算法 = 程序
结构化程序设计的缺点:程序在代码规模庞大时,变得难以理解、难以扩充、难以查错和难以复用。
第二节 面向对象程序设计的概念和特点
面向对象程序设计方法是20世纪90年代以来软件开发方法的主流,它继承了结构化程序设计方法的优点,同时又比较有效地改善了结构化程序设计的不足。
面向对象的程序设计方法: 使分析、设计和实现一个系统的方法尽可能地接近人们认识一个系统的方法。
通常包括3个方面: 面向对象的分析、面向对象的设计和面向对象的程序设计。
面向对象技术把问题看成是对象的集合。对象具有两个特性:一是对象本身的信息,也称为属性; 一是对对象的操作,也称为行为。
对象 = 数据(属性) + 函数(行为)。
类是对象的一个抽象。
面向对象程序设计的四个基本特点:抽象、封装、继承和多态。
1)抽象:将同一类事物的共同特点概括出来,这个过程就叫作"抽象".
类是对现实世界中客观事物的抽象。对于一个具体的类,它有许多个具体的个体,这些个体叫做“对象”
对象是系统中用来描述客观事物的一个实体,用对象名、属性和操作三要素来描述对象。
描述属性的数据称为成员变量或数据成员,对这组属性进行操作的函数称为成员函数。对象由数据成员和成员函数构成。
2)封装:就是把对象的属性和操作结合成一个独立的单元。
封装作用:
数据和操作数据的函数紧密联系起来;
将对象的一部分属性和函数隐藏起来,让这部分属性和函数对外不可见,起到保护作用。
另一些属性和函数对外可见,作为对对象进行操作的接口。
3)继承:在编写一个"新"类的时候,以现有的类作为基础,使得新类从现有的类“派生”而来,从而达到代码扩充和代码复用的目的。
4)多态:不同的对象可以调用相同名称的函数,但可导致完全不同的行为的现象称为多态性。利用多态性,程序中只需进行一般形式的函数调用,函数的实现细节留给接受函数调用的对象,这大提高了人们解决复杂问题的能力。
第三节 类的初步知识
1.类的基本概念
类是具有相同的属性和操作的一组对象的集合,它为属于该类的全部对象提供了统一的抽象描述,其内部包括属性(数据变量)和操作(成员函数)两个主要部分。
类是一种用户自己构造的数据类型;
类要先声明后使用,是具有唯一标识符的实体。
2.声明类
c++中声明类的一般形式为:
class 类名 {
private:
私有变量和函数
public:
公有变量和函数
protected:
保护变量和函数
};
说明:
①private是完全私有的,只有本类内可以访问,派生类和外部都不可以访问;
②protected是受保护的,只有本类内和派生类可以访问,外部不能访问;
③public内、外部都能访问;
④如无访问权限的关键字,则默认声明为private;
⑤不能在类的声明中对数据变量进行初始化;
⑥类声明中可以给出成员函数的参数的默认值;
⑦类中声明的任何成员不能使用extern、auto和register关键字修饰;
⑧类中可以不念有任何成员变量和成员函数,这样的类称为空类。
例
class Point //class是定义类的关键字,Point是类名
{
private: //声明私有访问权限
int x, y; //私有数据成员
public: //声明为公胡访问权限
void Setxy(int a, int b); //声明无返回值的公有成员函数
void Move(int a, int b); //声明无返回值的公有成员函数
void Disp(); //声明无返回值的公有成员函数
int Getx(); //声明返回值为int的公胡成员函数
int Gety(); //声明返回值为int的公有成员函数
}; //类声明以分号结束
3.定义成员函数
返回值类型 类名 :: 成员函数名(形参列表)
{
成员函数的函数体
}
说明:
①返回类型是指这个成员函数返回值的类型;
②类名是指成员函数所属类的名字;
③"::"是作用域运算符,用于表示其后的成员函数属于这个特定的类;
例: 定义属于Point类的成员函数Setxy
void Point :: Setxy(int a, int b)
{x=a; y=b;}
④类中定义的成员函数允许重载。
⑤可以使用关键字inline将成员函数定义为内联函数(凡是出现调用该函数的地方,编译程序自动将其转换为该函数的函数体,不再在程序执行时调用该函数,大大提高了效率)。
如: inline int Point::Getx()
{return x;}
⑥如果在声明类的同时,在类体内给出成员函数的定义,就默认为内联函数,
如将声明Getx的语句"int Getx(); "改为"int Getx(){return x;}", 则Getx为内联函数.
例:定义日期类 myDate
class myDate
{
public:
myDate(); //构造函数
myDate(int, int, int); //构造函数
void setDate(int, int, int); //设置日期
void setDate(myDate); //设置日期
myDate getDate(); //获取日期
void setYear(int); //设置年
int getMonth(); //获取月
void printDate() const; //打印日期
private:
int year, month, day; //成员变量,表示年,月,日
};
//在类体外定义成员函数
myDate::myDate() //构造函数用来初始化
{
year=1970, month=1, day=1;
}
myDate::myDate(int y, int m, int d) //构造函数
{
year=y; month=m; day=d;
}
void myDate::setDate(int y, int m, int d) //用变量设置日期
{
year=y; month=m; day=d;
return;
}
void myDate::setDate(myDate oneD) //用对象设置日期
{
year=oneD.year;month=oneD.month; day=oneD.day;
return;
}
myDate myDate::getDate() //获取日期
{
return *this;
}
void myDate::setYear(int y) //设置年
{
year=y;
return;
}
int myDate::getMonth() //获取月
{
return month;
}
void myDate::printDate() const //打印日期
{
cout << year << "/" << month << "/" << day;
return;
}
定义学生类Student
class Student
{
public:
void setStudent(string, myDate); //设置学生信息
void setName(string); //设置姓名
string getName(); //获取姓名
void setBirthday(myDate); //设置生日
myDate getBirthday(); //获取生日
void printStudent() const; //打印信息
private:
string name; //姓名
myDate birthday; //生日
}; //string系统定义好的类,myDate前面例题定义的类。
//在类体外定义成员函数
void Student::setStudent(string s, myDate d) //设置学生信息
{
name = s;
birthday.setDate(d);
return;
}
void Student::setName(string n) //设置姓名
{
name = n;
return;
}
string Student::getName() //获取姓名
{
return name;
}
void Student::setBirthday(myDate d) //设置生日
{
birthday.setDate(d);
return;
}
myDate Student::getBirthday() //获取生日
{
return birthday;
}
void Student::printStudent() const //打印信息
{
cout << "姓名: " << name << "生日: ";
birthday.printDate(); // 调用类myDate的成员函数
cout << endl;
}
第四节 类的示例程序刨析
一、程序结构
一个完整的C++程序包括以下几部分
◆一个主函数,可以调用其他函数,但不能被调用,也称为主程序。
◆用户定义的任意多个的类及全局函数。
◆全局说明。在所有函数和类定义之外的变量说明及函数原型。
◆注释。
◆头文件。
二、成员变量与成员函数的定义
1.定员变量:一般均定义为私有访问权限,这样的成员仅能在本类内访问。类外需要通过访问本类函数来访问私有的成员变量。
2.成员函数的定义
返回值类型 类名::成员函数名(形参列表)
{
函数体
}
三、创建类对象的基本形式
◆定义一个普通对象,即类变量的基本方法有两种。
1) 类名 对象名;
类名 对象名(参数); //定义对象同时初始化
类名 对象名 = 类名(参数);
2) 类名 *对象指针名=new 类名;
类名 *对象指针名=new 类名();
类名 *对象指针名=new 类名(参数);
◆声明对象引用
类名 &对象引用名=对象;
◆声明对象指针
类名 *对象指针名=对象的地址;
◆声明对象数组
类名 对象数组名[数组大小];
第五节 访问对象成员
一、使用对象访问成员变量与调用成员函数
◆对象访问成员变量的格式
对象名.成员变量名
◆通过对象的指针访问成员变量的格式
对象的指针->成员名
◆通过对象的引用访问成员变量的格式:
对象的引用.成员变量名
◆调用成员函数的格式:
对象名.成员函数名(参数表)
对象的指针->成员函数名(参数表)
对象的引用.成员函数名(参数表)
例: 验证类功能的驱动程序
#include <iostream>
#include <string>
using namespace std;
int main() {
Student ss;
int y, m, d;
string name_;
cout << "请输入学生的姓名和生日, 生日以'年 月 日'的次序输入:";
cin >> name_ >> y >> m >> d;
ss.setStudent(name_, myDate(y, m, d)); //调用成员函数
ss.printStudent();
return 0;
}
二、使用指针访问对象的成员
例: 使用指针方式的驱动程序
int main() {
Student ss;
int y, m, d;
string name_;
Student* sp = &ss; //对象的指针
cout << "请输入学生的姓名和生日,生日以'年 月 日'的次序输入:";
cin >> name_ >> y >> m >> d;
sp->setStudent(name_, myDate(y, m, d)); //调用成员函数
sp->printStudent();
return 0;
}
三、使用引用访问对象的成员
例:使用引用方式的驱动程序
int main(){
Student ss;
int y, m, d;
string name_;
Student& sy = ss; //对象的引用
cout << "请输入学生的姓名和生日,生日以'年 月 日'的次序输入:";
cin >> name_ >> y >> m >> d;
sy.setStudent(name_, myDate(y, m, d)); //调用成员函数
sy.printStudent();
return 0;
}
第六节 类成员的可访问范围
一、访问范围说明符的含义
public: 是公有的,使用它修饰的类的成员可以在程序的任何地方被使用。
private: 是私有的,使用它修饰的类的成员仅能在本类内被访问。
protected: 是保护的,它的作用介于public与private之间,使用它修饰的类的成员能在本类内及子类中被访问。
一般会将成员变量声明为私有的,而相关的成员函数声明为公有的,以便在类的外部可以调用这些成员函数来操纵成员变量。
二、成员的访问
类成员的访问
#include <iostream>
using namespace std;
class Box
{
public:
double length;
void setWidth(double wid);
double getWidth();
private:
double width;
};
//类体外定义成员函数
double Box::getWidth()
{
return width;
}
void Box::setWidth(double wid)
{
width = wid;
}
int main()
{
Box box;
box.length = 10.0; //正确,因为length是公有的
cout << "Length of box: " << box.length << endl;
//box.width = 10.0; //错误,因为width是私有的
box.setWidth(10.0); //必须使用成员函数设置宽度
cout << "Width of box:" << box.getWidth() << endl;
return 0;
}
三、隐藏的作用
设置私有成员的机制叫作“隐藏”。
“隐藏”的优点:
1)有利于程序的修改
2) 可以避免对对象的不正确操作
第七节 标识符的作用域与可见性
标知符的作用域
C++中标识符的作用域有函数原型作用域、局部作用域(块作用域)、类作用域和命名空间作用域。
1.函数原型作用域: 在声明函数原型时形参的作用范围就是函数原型作用域,这是C++程序中最小的作用域.
例: double ares(double r); r的作用范围就在函数area形参列表的左右括号之间。
2.局部作用域:程序中使用相匹配的一对大括号括起来的一段程序称为块。作用域局限在块内的称为局部作用域。具有局部作用域的变量也称为局部变量。
3.类作用域
设: m是类X的成员,x是类X的对象,ptr是指向类X的一个对象的指针,则对m的访问方式有如下2种:
1) 类X的成员函数中可直接使用m。(除非成员函数中有同名变量)
2)在类外,可以通过表达式x.m、X::m或者ptr->m来访问m。但注意不能违返m的访问修饰符的限定。
4.命名空间作用域
命名空间是为了消除程序各大模块之间同名引起的歧义。
定义命名空间的一般形式如下:
namespace 命名空间名
{
命名空间内的各种声明(函数声明\类声明、....)
}
在命名空间内部可以直接引用当前命名空间中声明的标识符,如果需要引用其他命名空间的标识符,需要使用下面的方式:
命名空间名:标识符名
C++提供两种形式using语句:
1) using命名空间名::标识符名;
2)using namespace 命名空间名;
使用该语句后,可以在当前作用域中直接引用该命名空间内的任何标识符。
例: using namespace std;
具有命名空间作用域的变量也称为全局变量。
标识符的可见性
标识符的可见性的一般原则:
▶标识符要声明在前,引用在后。
▶在同一个作用域中,不能声明同名的标识符。在没有互相包含关系的不同作用或中声明的同名标识符,互不影响。
▶如果存在两个或多个具有包含关系的作用域,外层声明了一个标识符,而内层没有再次声明同名标识符,那么外层标识符在内层仍然可见。如果在内层声明了同名标识符,则外层标识符在内层不可见, 这时称内层标识符隐藏了外层同名标识符,这种机制称为隐藏规则。
例:作用域隐藏规则
#include <iostream>
using namespace std;
int main()
{
int a = 1;
cout << a << "\n"; //输出1
for (int i = 1; i < 2; i++)
{
int a = 2;
cout << a << "\n"; //输出2
}
cout << a << "\n";//输出1
return 0;
}
第三章 类和对象进阶
第一节 构造函数
一、构造函数的作用
▶构造函数是类中的特殊成员函数。
▶构造函数的作用是完成对象的初始化。
▶给出类定义时,由程序员编写构造函数。如果程序员没有编写类的任何构造函数,则由系统自动添加一个不带参数的构造函数。
二、构造函数的定义
▶构造函数的类体里的声明形式:
类名(形参1,形参2,...形参n); //也可没有形参
▶构造函数的定义形式:
假设数据成员x1,x2,...xn, 类体外定义构造函数时通常有3种形式:
①类名::类名(形参1,形参2,..., 形参n):x1(形参1), x2(形参2),xn(形参n){ }
②类名::类名(形参1, 形参2, ..., 形参n)
{
x1 = 形参1;
x2 = 形参2;
...
xn = 形参n;
}
③类名::类名() //成员变量所赋的初值都是固定的
{
x1 = 初始化表达式1;
x2 = 初始化表达式2;
....
xn = 初始化表达式n ;
}
说明:
①构造函数的名字必须和类名相同;
②在定义构造函数时不能指定返回类型,即不要返回值,即使是void类型也不可以;
③另外类可有多个构造函数,即函数重载;或重载
④构造函数的参数在排列时无顺序要求,只要保证相互对应即可。
⑤构造函数可以使用默认参数。
⑥在程序中说明一个对象时,程序自动调用构造函数来初始化该对象。
例:定义日期类myDate1,
class myDate1
{
public:
myDate1(); //声明构造函数
myDate1(int); //声明构造函数
myDate1(int, int); //声明构造函数
myDate1(int, int, int); //声明构造函数
private:
int year, month, day; //成员变量
};
//定义构造函数
myDate1::myDate1() //固定值初始化
{
year = 1970;
month = 1;
day = 1;
}
myDate1::myDate1(int d):year(1970), month(1) //带1个参数
{
day = d;
}
myDate1::myDate1(int m, int d) :year(1970) //带2个参数
{
month = m;
day = d;
}
myDate1::myDate1(int y, int m, int d) //带3个参数
{
year = y;
month = m;
day = d;
}
myDate1::myDate1() //形式3
{
year = 1970;
month = 1;
day = 1;
}
可以改写为:形式1
myDate1::myDate1():year(1970),month(1),day(1){}
myDate1::myDate1(int y, int m, int d) //形式2
{
year = y;
month = m;
day = d;
}
可以改写为:形式1
myDate1::myDate1(int y, int m, int d) :year(y), month(m), day(d) {}
三、构造函数的使用
▶ 当程序创建一个对象时,系统自动调用构造函数来初始化该对象。
例: 使用构造函数创建类的对象
myDate1 d(); //使用无参的构造函数
myDate1 d1(25); //使用1个参数的构造函数
myDate1 d2(10, 20);
myDate1 d3(1971, 8, 18);
例: 使用构造函数的默认参数创建对象
myDate::myDate(int y=1970, int m=2, int d=14)
{
year=y;
month=m;
day=d;
}
myDate d(); //使用无参的构造函数
myDate d1(1980); //使用1个参数的构造函数
myDate d2(1990, 3);
myDate d3(2000, 4, 18);
例: 使用构造函数创建对象指针
myDate::myDate(int y=1970, int m=2, int d=14) //默认参数
{
year = y; month=m; day=d;
}
myDate *pd = new myDate(); //所指对象的值 1970/2/14
myDate *pd1 = new myDate(1955); //1955/2/14
myDate *pd2 = new myDate(1955, 4); //1955/4/14
myDate *pd3 = new myDate(1955, 5, 13); //1955/5/13
注意:使用new创建对象时,下面两种都是合法的:
myDate *pd=new myDate(); //带括号
myDate *pd=new myDate; //不带括号
▶用户定义了构造函数,都会调用构造函数进行初始化;
▶用户未定义构造函数,对带括号的情况,系统在为成员变量分配内存的同时,将其初始化为0。不加括号时,系统只为成员变量分配内存空间,但不进行内存的初始化,成员变量的值是随机值。
例: 使用构造函数创建对象数组
myDate::myDate(int y=1970, int m=2, int d=14)
{
year=y; month=m, day=d;
}
myDate A[3]; //对象A[0]、A[1]、A[2]的值都是1970/2/14
myDateB[3] = {myDate(1980), //对象B[0]的值是1980/2/14
myDate(1990, 12), //对象B[1]的值是1990/12/14
myDate(1980, 9, 10)}; //对象B[2]的值是1980/9/10
四、复制构造函数与类型转换构造函数
1.复制构造函数
▶复制构造函数是构造函数的一种,也称为拷贝构造函数。
▶复制构造函数的作用:使用一个已存在的对象去初始化另一个正在创建的对象
▶复制构造函数其原型为:
类名::类名 (类名&) //对象的引用作为形参
或 类名:: 类名 (const 类名 &) //为了不改变原有对象,使用const限制。
▶如果类中没有给出复制构造函数,那么编译器会自动生成一个默认复制构造函数。
例: 调用复制构造函数
Student stud;
Student ss[2] = {stud, Student()}; //创建ss[0]中的对象时,用到了默认复制构造函数。
第二条语句也可以写为如下的三条语句:
Student ss[2];
ss[0] = Student(stud); //调用默认复制构造函数
ss[1] = Student(); //调用构造函数
▶声明和实现复制构造函数的一般格式:
class 类名
{
public:
类名(形参表); //构造函数
类名(类名 &对象名); //声明复制构造函数
···········
};
类名:: 类名(类名 &对象名) //复制构造函数的实现
{
函数体
}
例: 自定义复制构造函数
在类Student中声明复制构造函数的原型:
Student(const Student &s);
Student::Student(const Student &s)
{
name = "copy" + s.name;
birthday = s.birthday;
}
主函数中有如下的语句:
Student stud; //定义对象stud并调用构造函数初始化
Student stud2 (stud); //定义对象stud2并调用复制构造函数初始化
Student stud3=stu; //定义对象stud3并调用复制构造函数初始化
Student ss[3] = {Student(), stud, Student(stu)}; //Student() 调用构造函数为ss[0]初始化
/* stud, Student(stu)作用一样,都是调用复制构造函数为ss[1]和ss[2]初始化,结果也是一样的*/
2.类型转换构造函数
只有一个参数的构造函数(复制构造函数除外)都可以称为类型转换构造函数。类型转换构造函数能够使一个其他类型的变量或常量自动转换成一个临时对象。类型转换构造函数实现的是从参数类型到该类类型的转换,函数带一个参数。
第二节 析构函数
▶析构函数和构造函数、复制构造函数都是构造型成员函数的基本成员;
▶析构函数的作用是在对象消失时,释放由构造函数分配的内存;
▶析构函数在类体里的声明形式: ~类名()
▶析构函数的定义形式:类名::~类名(){}
▶类只能定义一个析构函数,且不能指明参数;
▶如果程序中没有定义析构函数,则编译器自动生成默认的析构函数,默认析构函数的函数体为空。
▶使用new运算符动态分配了内存空间,则在析构函数中应该使用delete释放掉这部分占用的空间。
▶当程序先后创建几个对象时,系统按照后建先析构的原则析构对象,当使用delete调用析构函数时,则按delete的顺序析构。
▶析构函数在对象的生存期结束时被编译系统自动调用,然后对象占用的内存被回收。
例:
#include <iostream>
using namespace std;
class myDate2
{
public:
myDate2();
~myDate2();
void printDate() const; //打印日期
private:
int year, month, day;
};
myDate2::myDate2()
{
year = 1970, month = 4, day = 2;
cout << "myDate2构造函数1" << endl;
}
myDate2::~myDate2() //定义析构函数
{
cout << "myDate2析构函数" << endl;
}
void myDate2::printDate() const //打印日期
{
cout << year << "/" << month << "/" << day;
return;
}
class Student1
{
public:
Student1();
Student1(string, myDate2);
Student1(const Student1& s);
~Student1();
void printStudent1(); //打印信息
private:
string name;
myDate2 birthday;
};
Student1::Student1() : name("Noname"), birthday(myDate2())
{
cout<<"Student1构造函数0"<< endl;
}
Student1::Student1(const Student1& s) //复制构造函数
{
name = "copy" + s.name;
birthday = s.birthday;
cout<< "Student1复制构造函数" << endl;
}
Student1::~Student1() //定义析构函数
{
cout << "Student1析构函数" << endl;
}
void Student1::printStudent1() //打印信息
{
cout << "姓名: " << name << " 生日: ";
birthday.printDate(); // 调用类myDate的成员函数
cout << endl;
}
int main()
{
Student1 student1;
student1.printStudent1(); //输出默认值
Student1 sy[2] = {Student1(), student1};
for (int i = 0; i < 2; i++)
{
sy[i].printStudent1();
}
return 0;
}
例: 对象数组与delete语句
int main()
{
Student1 student1;
student1.printStudent1(); //输出默认值
Student1* ss = new Student1[2]; //ss指向新申请对象数组
for (int i = 0; i < 2; i++)
{
(ss + i)->printStudent1();
}
delete []ss;
return 0;
}
例: 指针数组与delete语句
int main()
{
Student1 student1;
student1.printStudent1(); //输出默认值
Student1* ss[2] = {new Student1(), new Student1()}; //ss是对象指针数组
for (int i = 0; i < 2; i++)
{
ss[i]->printStudent1();
}
delete ss[0];
delete ss[1];
return 0;
}
第三节 类的静态成员
一、静态变量
▶ 静态变量根据变量定义的位置不同,分为静态全局变量和静态局部变量。
▶静态全局变量: static修修饰的、在所有花括号之外声明的变量,其作用域范围是全局可见的,即在整个项目文件内都有效。
▶静态局部变量:static修饰的、块内定义的变量,其作用域从定义之处开始到本块结束处为止。
▶静态变量均存储在全局数据区,静态局部变量只执行一次初始化。如果静态变量未初始化,则系统将其初始化为0。
例:自动变量和静态变量的定义和使用
#include <iostream>
using namespace std;
static int glos = 100; //静态全局变量
void f()
{
int a = 1; //局部自动变量a
static int fs = 1; //静态局部变量fs,完成初始化
cout << "在f中: a(自动)=" << a << " fs(静态)=" << fs << " glos(静态)=" << glos << endl;
a += 2; fs += 2; glos += 10;
cout << "在f中: a(自动)=" << a << " fs(静态)=" << fs << " glos(静态)=" << glos << endl << endl;
//cout<<"ms="<<ms<<endl; //该行错误,变量ms不可见
};
int main()
{
static int ms = 10;
for (int i = 1; i <= 3; i++)
{
f();
}
//cout<<<<fs<<endl; //该行错误,变量fs不可见
cout << "ms=" << ms<<endl; //输出
cout << "glos=" << glos << endl;
return 0;
}
二、类的静态成员
▶ 类的静态成员有两种:静态成员变量和静态成员函数。
▶ 定义静态成员:在类体内定义成员时,在前面加上static。
▶ 静态成员变量不能在类体内赋值。 给静态成员变量赋初值的格式如下:
类型 类名::静态成员变量=初值; //不能有static
▶ 在类体外定义成员函数时,前面也不能加static。
▶ 类的静态成员被类的所有对象共享。
▶ 静态函数与静态函数之间、非静态函数与非静态函数之间是可以相互调用的,非静态成员函数内不能调用非静态成员函数。
▶ 访问类静态成员的一般格式如下:
类名 :: 静态成员名
对象名.静态成员名
对象指针->静态成员名
例: 静态成员的使用
#include <<iostream>>
using namespace std;
class classA
{
public:
double x, y;
static int num; //公胡静态成员变量供所有对象"共有"
//用于记录通过构造函数已生成的对象个数
classA() //构造函数
{
x = 0; y = 0;
num++; //每生成一个对象, num加1
}
classA(double x0, double y0) //构造函数
{
x = x0; y = y0;
num++;
}
static void staticFun() //静态成员函数
{
cout << "current num=" << num << endl;
//cout<<"x="<<x<<endl; //错误,在静态函数中不能访问非静态变量
}
};
int classA::num = 0; //必须在类体外初始化静态
int main()
{
classA obj(1.2, 3.4), * p; //调用1次构造函数
cout << "classA::num=" << classA::num << endl; //使用类名做限定符
classA::staticFun(); //调用函数
cout << "obj.num=" << obj.num << endl; //使用对象名做限定符
obj.staticFun();
cout << endl;
classA A[3]; //调用3次构造函数
cout << "class::num=" << classA::num << endl;
classA::staticFun();
cout << endl;
p = new classA(5.6, 7.8);
cout << "classA::num=" << classA::num << endl;
classA::staticFun();
cout << "p->num=" << p->num << endl;
p->staticFun();
return 0;
}
静态成员函数与一般成员函数的不同
(1) 可以不指向某个具体的对象,只与类名连用;
(2) 在没有建立对象之前,静态成员就已存在;
(3) 静态成员是类的成员,不是对象的成员 ;
(4) 静态成员为该类的所有对象共享,它们被存储于一个公用内存中;
(5) 没有this指针,只能通过对象名或指向对象的指针访问类的数据成员。
(6) 静态成员函数不能被说明为虚函数;
(7) 静态成员函数不能直接访问非静态函数。
静态对象与普通对象区别
(1) 静态对象的构造函数在代码执行过程中,在第一次遇到它的变量定义并初始化时被调用,但直到整个程序结束之前仅调用一次;而普通对象则是遇到变量定义就被调用,遇到几次调用几次。
(2) 静态对象的析构函数在整个程序退出之前被调用,同样也只调用一次; 而普通对象则是变量被定义几次,则析构几次。
第四节 变量及对象的生存期和作用域
一、变量的生存期和作用域
▶ 变量的生存期: 是指变量所占据的内存空间由分配到释放的时期。
▶ 变量的作用域: 变量有效的范围
▶ 全局变量: 是程序中定义在所有函数之外的变量。其作域是程序从变量定义到整个程序结束的部分; 其生存期为整个程序的执行期间。
▶ 局部变量:在函数内或程序块内说明的变量。其作用域是函数体内或程序块内(一对大括号括起来的程序段); 其生存期为从被说明处开始,到所在函数或程序块结束处结束。
二、类对象的生存期和作用域
▶ 类的对象在生成时调用构造函数,在消亡时调用析构函数,在这两个函数调用之间即是对象的生存期。
▶ 类的对象的作用域和变量的作用域含义一致。
第五节 常量成员和常引用成员
1.类常量成员变量
▶ 类常量成员变量:由关键字const修饰的类成员变量
▶ 定义类常量成员变量的一般格式如下:
const 数据类型 类常量成员变量 = 表达式
▶ 类常量成员变量 必须进行初始化,而且只能通过构造函数的成员初始化列表的方式进行。
2、常量对象
▶ 常量对象:使用const声明的对象就是常对象
▶常量对象必须在声明的同时进行初始化,而且不能被更新。定义常量对象的一般格式:
const 类名 对象名 (参数表) ; 或 类名 const 对象名(参数表);
例: myDate const al(1977, 12, 20); //定义myDate类常量对象al并初始化
▶ 常量对象只能调用常量成员函数,不能调用非常量函数,普通对象可以调用所有成员函数
3、常量函数
▶ 常量函数:用const声明、定义的成员函数。
▶ 常量函数的声明及定义形式为:
(1) 在类体内定义常量函数为内联函数时的形式: 类型标识符 函数名 (参数列表) const {......//函数体}
(2)在类体内声明,类体外定义时的形式
声明形式:类型标识符 函数名 (参数列表) const;
定义形式:类型标识符::函数名 (参数列表) const {......//函数体}
4、常引用作为函数的参数
使用引用作为函数参数,传送的是地址,所以形参改变,则实参也跟着改变,但如果不希望函数改变对象的值,就要使用常引用作为参数。
例如: void Display(const double& r) {cout<<r<<endl;} //Display只能使用而不能改变r所引用的对象。
例: 调用常量成员函数与普通成员函数
#include <iostream>
using namespace std;
class Sample
{
public:
Sample();
void getValue() const; //常量成员函数
void getValue(); //非常量成员函数(同名)
void priValue(); //非常量成员函数
void priVcon() const; //常量成员函数
};
Sample::Sample() {}; //构造函数
void Sample::getValue() const //常量成员函数
{
cout << "常量成员函数" << endl;
}
void Sample::getValue() //非常量成员函数
{
cout << "非常量成员函数" << endl;
}
void Sample::priValue() //非常量成员函数
{
cout << "非常量成员函数" << endl;
}
void Sample::priVcon() const //常量成员函数
{
cout << "常量成员函数" << endl;
}
int main()
{
const Sample cono;
Sample o;
cout << "cono\t";
cono.getValue(); //通过常量对象只能调用常量成员函数
//cono.priValue(); //错误,不能调用非常量成员函数
cout << "o\t";
o.getValue(); //"同名"情况下,系统自动调用区别
cout << "o\t";
o.priValue(); //调用普通函数
cout << "o\t";
o.priVcon(); //调用常量成员函数
return 0;
}
例:常量成员变量及常量成员函数的使用
#include <iostream>
using namespace std;
class constClass
{
const int conMbr; //类中的常量成员变量
int Mbr; //普通成员变量
public:
constClass() : conMbr(0), Mbr(100) {} //类中定义的const成员变量必须在构造函数的初始化列表中进行初始化
constClass(int i) : conMbr(i)
{
Mbr = 200; //初始化列表处给出初值
}
void printConst()
{
cout << "conMbr=" << conMbr << ", Mbr=" << Mbr << endl;
}
int getConst()
{
cout << "调用非常量函数" << endl;
return Mbr;
}
int getConst() const
{
cout << "调用常量函数" << endl;
return conMbr;
}
int getValue()
{
return Mbr;
}
void processConst()
{
cout << "--在processConst函数中非常量--" << endl;
int x = 2 * conMbr + 1; //可以读取conMbr
cout << "x=2*conMbr+1=" << x << endl;
//conMbr++; //错误! 不能更改常量成员变量 conMbr 的值
Mbr++; //可以修改非常量Mbr的值
cout << "Mbr=" << Mbr << endl;
}
void processConst() const
{
cout << "--在processConst函数中常量--" << endl;
int x = conMbr + 1;
cout << "x=conMbr+1=" << x << endl; //可以读取conMbr
//conMbr++; //错误! 不能更改常量成员变量conMbr的值
//Mbr++; //错误! 也不能更改非常量成员变量Mbr的值
cout << "Mbr=" << Mbr << endl;
}
};
int main()
{
constClass ob1(123), ob2; //定义普通对象 ob1={123, 200}, ob2={0, 100}
ob1.printConst();
cout << "ob2.getConst=" << ob2.getConst() << endl; //普通对象系统自动调用非常量函数
ob2.processConst();
const constClass ob3(20); //定义常量对象ob3={20, 200}
cout << "ob3.getConst()=" << ob3.getConst() << endl;
ob3.processConst();
return 0;
}
例: 类中的常引用型成员变量
#include <iostream>
using namespace std;
int fvalue=10;
class CDemo
{
public:
const int num; //常量型成员变量
const int& ref; //常量引用型成员变量
int value;
public: CDemo(int n) :num(n), ref(value), value(4) {} //常引用型成员变量也必须在构造函数的初始化列表中进到进行初始化
};
int main()
{
cout << sizeof(CDemo) << endl; //输出类CDemo对象的长度 24(整型占8个字节)
CDemo fn(100); //定义对象f,初始化f{100, 4, 4}
//f.ref = f.value; //错误! 在程序中不能修改这个引用
cout << "fn.num=" << fn.num << "\tfn.ref=" << fn.ref << "\tfn.value=" << fn.value << endl;
return 0;
}
第六节 成员对象和封闭类
▶ 一个类的成员变量如果是另一个类的对象, 则该成员变量称为“成员对象”。这两个类为包含关系。包含成员对象的类叫作封闭类。
▶ 上面定义的类Student和myDate类,类Student中的成员变量birthday是类myDate的对象,birthday就是成员对象,Student是封闭类。
一、封闭类构造函数的初始化列表
▶ 在定义封闭类的构造函数时,需要添加初始化列表,指明要调用成员对象的哪个构造函数。
▶ 在封闭类构造函数中添加初始化列表的格式如下:
封闭类名: 构造函数名(参数表) : 成员变量 1 (参数表), 成员变量2 (参数表), ... {...}
例: Student:: Student(string n):name(n), birthday(myDate()) { }
▶ 执行封闭类的构造函数时,先执行成员对象的构造函数,然后再执行本类的构造函数。
例: 封闭类的定义
#include <iostream>
using namespace std;
class CTyre //轮胎类
{
private:
int radius; //半径
int width; //宽度
public:
CTyre() : radius(16), width(185) {} //定义构造函数1
CTyre(int r, int w) : radius(r), width(w){} //定义构造函数2
int getRadius() //获取半径
{
return radius;
}
int getWidth() //获取宽度
{
return width;
}
};
class CCar //汽车类, 封闭类
{
private:
int price; //价格
CTyre tyre; //成员对象
public:
CCar(); //定义构造函数3
CCar(int p, int tr, int tw); //声明构造函数3
int getPrice() //获取价格
{
return price;
}
CTyre getCTre() //获取轮胎信息
{
return tyre;
}
};
CCar::CCar() //定义构造函数3
{
price = 50010;
CTyre();
};
CCar::CCar(int p, int tr, int tw) :price(p), tyre(tr, tw) {}; //定义构造函数4,使用初始化列表
int main()
{
CCar car(48900, 17, 225); //定义对象car并调用构造函数4初始化,
//需要先执行构造函数2初始化轮胎信息,再继续执行构造函数3初始化价格
cout << "price=" << car.getPrice();
cout << "\tCTyre.Radius=" << car.getCTre().getRadius()
<< "\tCTyre.Width=" << car.getCTre().getWidth() << endl;
CCar car1; //定义对象car并调用构造函数3初始化,需要先执行构造函数1初始化轮胎信息,
//再继续执行构造函数3初始化价格
cout << "price=" << car1.getPrice();
cout << "\tCTyre.Radius=" << car1.getCTre().getRadius()
<< "\tCTyre.Width=" << car1.getCTre().getWidth();
return 0;
}
▶ 封闭类对象生成时,先执行所有成员对象的构造函数,然后执行封闭类自己的构造函数。
▶ 成员对象构造函数的执行次序与成员对象在类定义中的说明次序一致,与它们在构造函数初始化列表中出现的次序无关。
▶ 当封闭类对象消亡蝗,先执行封闭类的析构函数,然后再执行成员对象的析构函数,成员对象析构函数的执行次序和构造函数的执行次序相反,即先构造的后析构。
例: 封闭类对象的创建与消亡
class CTyre //轮胎类
{
private:
int radius; //半径
int width; //宽度
public:
CTyre() :radius(16), width(185) //构造函数1(内联)
{
cout << radius << "\tCTyre 构造函数1 " << endl;
}
CTyre(int r, int w) :radius(r), width(w) //构造函数2(内联)
{
cout << radius << "\tCTyre 构造函数2" << endl;
}
~CTyre()
{
cout << radius << "\tCTyre析构函数1" << endl; //析构函数1
}
int getRadius() //获取半径
{
return radius;
}
int getWidth() //获取宽度
{
return width;
}
};
class CCar //汽车类,封闭类
{
private:
int price; //价格
CTyre1 tyre; //成员对象
public:
CCar(); //声明构造函数3
CCar(int p, int tr, int tw); //声明构造函数4
~CCar(); //声明析构函数2
int getPrice() //获取价格
{
return price;
}
CTyre getCtyre() //获取轮胎信息
{
return tyre;
}
};
CCar::CCar() //在类外定义构造函数3
{
price = 50010;
CTyre();
cout << price << "\tCCar1构造函数3" << endl;
}
CCar::CCar(int p, int tr, int tw) :price(p), tyre(tr, tw) //在类外定义构造函数4, 使用初始化列表
{
cout << price << "\tCCar构造函数4"<<endl;
}
CCar::~CCar() //类外定义析构函数
{
cout << price << "\tCCar析构函数2" << endl;
}
int main()
{
CCar car(48900, 17, 225);
cout << "price=" << car.getPrice() << endl;
cout << "CTyre.Radius=" << car.getCtyre().getRadius() << endl;
cout << "CTyre.Width=" << car.getCtyre().getWidth()<<endl;
CCar car2;
cout << "price=" << car2.getPrice() << endl;
cout << "CTyre.Radius=" << car2.getCtyre().getRadius() << endl;
cout << "CTure.Width=" << car2.getCtyre().getWidth() << endl;
return 0
}
二、封闭类的复制构造函数
如果封闭类的对象是用默认复制构造函数初始化的那么,它包含的成员对象也会用复制构造函数初始化。
例:复制构造函数的使用
#include <iostream>
using namespace std;
class A
{
public:
A() //构造函数
{
cout << "defeult" << endl;
}
A(A& a) //复制构造函数
{
cout << "copy" << endl;
}
};
class B //封闭类
{
A a;
};
int main()
{
B b1, b2(b1);
return 0;
}
▶ b1:是用类A的构造函数初始化
▶ b2(b1): 是用类A的复制构造函数初始化的。
第七节 友元
一、友元
▶ 友元是为了兼顾C语言程序设计的习惯与C++信息隐藏的特点,而特意增加的功能
▶ 友元机制是对一些类外的函数打开的一个特殊通道,授权它们能够访问本类的私有成员变量。
▶ 友元的概念破坏了类的封装性和信息隐藏,但有助于数据共享,能够提高程序执行的效率。
▶ 友元机制包括友元函数和友元类。
二、友元函数
▶ 友元函数:在定义一个类的时候,可以把一些函数(包括全局函数 和其他类的成员函数)声明为“友元”, 这样那些函数就成为本类的友元函数。
▶ 在类定义中声明友元函数的形式:
friend 函数类型 函数名 (参数列表) ; //针对全局函数
friend 函数类型 函数所在类名:: 函数名 (参娄列表);
▶ 友元函数可在类中的私有或公有部分通过关键字friend声明或定义,但如在类中声明,而在类外定义,就能再在类外使用friend关键字。
▶ 友元函数应被看作类的接口的一部分,使用它的主要目的是提高效率,因为它可以直接访问对象的私有成员,从而省去调用类的相应成员函数的开销。
▶ 友元函数的另一个优点是: 类的设计者不必在考虑好该类的各种可能使用情况之后再设计这个类,而是可以根据需要,通过使用友元来增加类的接口。
例:友元函数的声明
#include <iostream>
#include <cmath>
using namespace std;
class Pixel; //前向引用声明
class Test
{
public:
void printX(Pixel p); //用到了类Pixel
};
class Pixel
{
private:
int x, y;
public:
Pixel(int x0, int y0) //定义构造函数
{
x = x0;
y = y0;
}
void printxy()
{
cout << "pixel:( " << x << " . " << y << " )" << endl;
}
friend double getDist(Pixel p1, Pixel p2); //声明全局友元函数
friend void Test::printX(Pixel p); //声明text类的成员函数为友元函数
};
void Test::printX(Pixel p) //定义Text类的成员函数
{
cout << "x=" << p.x << "\ty=" << p.y << endl;
return;
}
double getDist(Pixel p1, Pixel p2) //友元函数在类外定义
{
double xd = double(p1.x - p2.x); //友元函数使用piexl类私有成员
double yd = double(p1.y - p2.y); //强制类型转换
return sqrt(xd * xd + yd * yd); //计算两点之间距离
}
int main()
{
Pixel p1(0, 0), p2(10, 10);
p1.printxy();
p2.printxy();
cout << "(p1, p2)间距离=" << getDist(p1, p2) << endl; //直接调用全局函数
Test t;
cout << "从友元函数中输出---" << endl;
t.printX(p1); //通过对象调用类的成员函数
t.printX(p2); //通过对象调用类的成员函数
return 0;
}
例: 友元函数实现复数类操作
#include <iostream>
using namespace std;
class myComplex //复数类
{
private:
double real, imag; //复数的实部,虚部
public:
myComplex(); //声明构造函数
myComplex(double r, double i); //声明构造函数
friend myComplex addCom(myComplex c1, myComplex c2); //友元函数,两个参数对象c1与c2相加
friend void outCom(myComplex c); //友元函数,输出参数对象c的有关数据
};
myComplex::myComplex() //定义构造函数
{
real = 0; imag = 0;
}
myComplex::myComplex(double r, double i) //定义构造函数
{
real = r, imag = i;
}
myComplex addCom(myComplex c1, myComplex c2) //类外定义友元函数,求两个复数的和
{
return myComplex(c1.real + c2.real, c1.imag + c2.imag);
}
void outCom(myComplex c) //类外定义友元函数,输出复数
{
cout << "(" << c.real << "." << c.imag << ")";
}
int main()
{
myComplex c1(1, 2), c2(3, 4), res;
res = addCom(c1, c2); //调用友元函数不通过类对象,计算复数的和
outCom(c1); //输出c1
cout << "+";
outCom(c2); // 输出c2
cout << "=";
outCom(res); //输出复数的和
cout << endl;
Pixel p1(0, 0), p2(10, 10);
p1.printxy();
p2.printxy();
cout << "(p1, p2)间距离=" << getDist(p1, p2) << endl; //直接调用全局函数
Test t;
cout << "从友元函数中输出---" << endl;
t.printX(p1); //通过对象调用类的成员函数
t.printX(p2); //通过对象调用类的成员函数
三、 友元类
▶ 如果将一个类B说明为另一个类A的友元类,则类B中的所有函数都是类A的友元函数。
▶在类定义中声明友元类的格式如下:
friend class 类名;
▶ 友元类的关系是单向的。若说明类B是类A的友元类,不等于类A也是类B的友元类、友元类的关系不能传递,即若类B是类A的友元类,而类C是类B的友元类,不等于 类C是类A的友元类。
▶ 除非确有必要, 一般不把整个类说明为友元类,而仅把类中的某些成员函数说明为友元函数。
例:友元类
#include <iostream>
using namespace std;
class myComplex //复数类
{
private:
double real, imag; //复数的实部,虚部
public:
myComplex(); //声明构造函数
myComple(double r, double i); //声明构造函数
friend class oper; //oper是myComplex的友元类
};
myComplex::myComplex() //定义构造函数
{
real = 0;
imag = 0;
}
myComplex::myComplex(double r, double i) //定义构造函数
{
real = r; imag = i;
}
class oper
{
public:
myComplex1 addCom(myComplex c1, myComplex c2); //声明成员函数,两个参数对象c1与c2相加
void outCom(myComplex c); //声明成员函数, 输出参数对象c的有关数据
};
myComplex oper::addCom(myComplex c1, myComplex c2) //类外定义函数
{
return myComplex(c1.real + c2.real, c1.imag + c2.imag);
}
void oper::outCom(myComplex c) //类外定义
{
cout << "(" << c.real << ", " << c.imag << ")";
}
int main()
{
myComplex c11(1, 2), c12(3, 4), res1;
oper o;
res1 = o1.addCom(c11, c12); //通过类oper操作类myComplex1
o1.outCom(c11);
cout << "+";
o1.outCom(c12);
cout << "=";
o1.outCom(res1);
cout << endl;
return 0;
}
第八节 this指针
一、this指针的实际形式
例如: 成员函数Setxy (int a, int b) {x =a, y=b;}
当执行A.Setxy(25, 55)时,为了确保正确赋值,成员函数Setxy(int a, int b)实际上是如下形式:
void Point::Setxy (int a, int b) //此时成员函数的this指针指向对象
{ this->x = a; this->y = b; }
注意: 除非形参的名字与成员变量的名字相同,一般情况下都省略掉符号"this->", 而让系统进行默认设置。
▶ c++规定,当一个成员函数被调用时,系统将自动向它传递一个隐含的参数,该参数是一个指向调用该函数的对象的指针,名为this指针,从而使成员函数知道该对哪个对象进行操作。
▶ 使用this指针,保证了每个对象可以拥有自己的数据成员,但处理这些数据成员的代码却可以被所有的对象共享,从而提高了程序的安全性和效率。
▶ this指针是C++实现封装的一种机制,它将对象和该对象调用的成员函数连接在一起,从而在外部看来,每个对象都拥有自己的成员函数。
▶ this指针指向的是成员函数作用的对象,也就是调用成员函数的对象。友元函数不通过对象调用,所能没有this指针。
▶ 静态成员 是类具有的属性,不是对象的特征,this表示的是隐藏的对象的指针,所以静态成员函数没有this指针。
第四章 运算符重载
第一节 运算符重载的概念
一 、重载运算符的概念
▶ 运算符重载: 给已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时产生不同的行为。
▶ 运算符重载的目的是使得C++中的运算符也能够用来操作对象。
▶ 可重载的运算符列在表,绝大多数运算符都可重载
▶ 不可重载的运算符及符号列在中,一共7种,要记住这7种运算符。

▶ 运算符重载的实质是编写以运算符为名称的函数,使用运算符的表达式就被解释为对重载函数的调用 。
▶ 运算符函数的格式如下:
返回值类型 operator 运算符 (形参表)
{ 函数体 }
▶ 运算符可以被重载为全局函数(通常为类的友元函数, 全局函数不能访问类的的私有成员),对于二元运算符,需要传递两个参数。
▶ 运算符可以被重载为类的成员函数,对于二元运算符只需要传递一个参数。
二、 重载运算符为类的成员函数
例: 为类 myComplex重载运算符 "+" 和 "-"
#include <iostream>
using namespace std;
class myComplex //复数类
{
private:
double real, imag; //复数的实部,虚部
public:
myComplex(); //构造函数
myComplex(double r, double i); //构造函数
void outCom(); //成员函数
myComplex operator-(const myComplex& c); //声明成员函数
friend myComplex operator+(const myComplex& c1, const myComplex& c2); //声明友元函数
};
myComplex::myComplex() //定义构造函数
{
real = 0; imag = 0;
}
myComplex::myComplex(double r, double i) //定义构造函数
{
real = r, imag = i;
}
void myComplex::outCom() //定义输出成员函数
{
cout << "(" << real << "." << imag << "),";
}
myComplex myComplex::operator-(const myComplex& c)
{
//重载成员函数,一个参数,返回一个临时对象,this可以省略
return myComplex(this->real - c.real, this->imag - c.imag);
}
myComplex operator+(const myComplex& c1, const myComplex& c2)
{
//重载友元函数,两个参数,返回一个临时对象
return myComplex(c1.real + c2.real, c1.imag + c2.imag);
}
int main()
{
myComplex c1(1, 2), c2(3, 4), res;
c1.outCom();
cout << "operator+";
c2.outCom();
cout << "= ";
res = c1 + c2; //调用运算符重载函数
res.outCom();
cout << endl;
c1.outCom();
cout << "operator-";
c2.outCom();
cout << "=";
res = c1 - c2;
res.outCom();
cout << endl;
return 0;
}
三、重载运算符为友元函数
▶ 将运算符重载为成员函数时有时满足不了需要。
例 重载为友元函数
#include <iostream>
using namespace std;
class myComplex //复数类
{
private:
double real, imag; //复数的实部,虚部
public:
myComplex(); //构造函数
myComplex(double r, double i); //构造函数
void outCom(); //成员函数
friend myComplex operator+(const myComplex& c1, const myComplex& c2); //声明友元函数
friend myComplex operator-(const myComplex& c1, const myComplex& c2);
friend myComplex operator-(const myComplex& c1, double r);
friend myComplex operator-(double r, const myComplex& c1);
};
myComplex::myComplex() //定义构造函数
{
real = 0; imag = 0;
}
myComplex::myComplex(double r, double i) //定义构造函数
{
real = r; imag = i;
}
void myComplex::outCom() //定义输出成员函数
{
cout << "(" << real << "," << imag << ")";
}
myComplex operator+(const myComplex& c1, const myComplex& c2)
{
//重载友元函数,两个参数,完成c1+c2
return myComplex(c1.real + c2.real, c1.imag + c2.imag);
}
myComplex operator-(const myComplex& c1, const myComplex& c2)
{
//重载友元函数,两个参数,完成c1-c2
return myComplex(c1.real - c2.real, c1.imag - c2.imag);
}
myComplex operator-(const myComplex& c1, double r)
{
//重载友元函数,两个参数,完成c1-r
return myComplex(c1.real - r, c1.imag);
}
myComplex operator-(double r, const myComplex& c1)
{
//重载友元函数,两个参数,完成r-c1
return myComplex(r - c1.real , -c1.imag);
}
int main()
{
myComplex c1(1, 2), c2(3, 4), res;
res = c1 - c2; // 理解为调用函数res= operator - (c1, c2)
cout << endl << "c1-c2=";
res.outCom();
res = c1 + c2;
cout << endl << "c1+c2=";
res.outCom();
res = c1 - 5;
cout << endl << "c1-5="; res.outCom();
res = 5 - c1;
cout << endl << "5-c1="; res.outCom();
return 0;
}
四、重载运算符的规则
1) 重载后运算符的含义应该符合原有的用法习惯。例如, 重载“+”运算符,完成的功能就应该类似于做加法,在重载的“+”运算符中做减法是不合适的。
2) 运算符重载不能改变运算符原有的语义,包括运算的优先级和结合性。
3) 运算符重载不能改变运算符操作数的个数及语法结构。不能创建新运算符,即重载运算符不能超出C++语言允许重载的运算符范围。
4) 重载运算符“()” "[]" "->" 或者赋值运算符"="时,只能将它们重载为成员函数。 不能重载为全局函数。
5) 运算符重载不能改变该运算符用于基本数据类型对象的含义。重载的运算符可以用于用户自定义类型的对象之间的运算,也可以用于用户自定义类型的对象与基本数据类型对象之间的混合运算。
第二节 重载赋值运算符
一、重载赋值运算符
▶ 用于类运算的运算符通常都要重载。但有两个运算符,系统提供了默认的重载版本。
1) 赋值运算符=:系统默认重载为对象成员变量的复制。
2) 地址运算符&: 系统默认重载为返回任何类对象的地址。
例如: c1和c2都是复数类的对象
c1 = c2; //合法,系统默认的重载赋值运算
c1 = 7; //错误,数据类型不同,需要编写相应的重载赋值运算符的函数。
例: 为类myComplex重载赋值运算符
#include <iostream>
using namespace std;
class myComplex //复数类
{
private:
double real, imag; //复数的实部,虚部
public:
myComplex(); //构造函数
myComplex(double r, double i); //构造函数
~myComplex() {} //析构函数
myComplex addCom(myComplex c1); //成员函数,调用对象与参数
void outCom(); //成员函数
void outCom(string s); //成员函数
void changeReal(double r); //声明成员函数
friend myComplex operator+(const myComplex& c1, const myComplex& c2); //声明友元函数, 实现c1+c2
friend myComplex operator+(const myComplex& c1, double r);
friend myComplex operator+(double r, const myComplex& c1);
friend myComplex operator-(const myComplex& c1, const myComplex& c2);
friend myComplex operator-(const myComplex& c1, double r);
friend myComplex operator-(double r, const myComplex& c1);
myComplex& operator=(const myComplex& c); //声明成员函数
myComplex& operator=(double); //声明成员函数
};
myComplex::myComplex() //定义构造函数
{
real = 0; imag = 0;
}
myComplex::myComplex(double r, double i) //定义构造函数
{
real = r; imag = i;
}
myComplex myComplex::addCom(myComplex c1) //定义成员函数,一个参数
{
return myComplex(this->real + c1.real, this->imag + c1.imag);
}
void myComplex::outCom() //定义成员函数,输出复数
{
cout << "(" << real << "." << imag << ")";
}
void myComplex::outCom(string s) //定义成员函数
{
cout << s << "=(" << real << "." << imag << ")" << endl;
}
void myComplex::changeReal(double r) //定义成员函数
{
this->real = r;
}
myComplex operator+(const myComplex& c1, const myComplex& c2)
{
return myComplex(c1.real + c2.real, c1.imag + c2.imag);
};
myComplex operator+(const myComplex& c1, double r)
{
return myComplex(c1.real+r, c1.imag);
}
myComplex operator+(double r, const myComplex& c1)
{
return myComplex(r + c1.real, c1.imag);
}
myComplex operator-(const myComplex& c1, const myComplex& c2)
{
return myComplex(c1.real - c2.real, c1.imag - c2.imag);
}
myComplex operator-(const myComplex& c1, double r)
{
return myComplex(c1.real - r, c1.imag);
}
myComplex operator-(double r, const myComplex& c1)
{
return myComplex(r - c1.real, -c1.imag);
}
myComplex& myComplex::operator=(const myComplex& c1)
{
this->real = c1.real; this->imag = c1.imag;
return *this;
} //赋值运算符重载成员函数
myComplex& myComplex::operator=(double r) //赋值运算符重载成员函数
{
this->real = r; this->imag = 0;
return *this;
}
int main()
{
myComplex c1(1, 2), c2(3, 4), res;
c1.outCom("\t\t\tc1");
c2.outCom("\t\t\tc2");
res = c1 + c2; //调用友元函数计算c1 + c2
res.outCom("执行 res = c1 + c2->\t res");
res = c1.addCom(c2); //调用成员函数计算c1 + c2;
res.outCom("执行res=c1.addCom(c2)->\tres");
res = c1 + 5; //调用友元函数计算c1 + 5
res.outCom("执行res=c1 + 5->\t\tres");
res = c1; //调用成员函数 operator=(const myComplex &c)
res.outCom("执行res=c1->\t\tres");
res = 7; //调用成员成员函数operator=(double r), 没有该函数,出错
res.outCom("执行res=7->\t\tres");
res = 7 + 8; //res=15, 调用成员函数operator=(double r)
res.outCom("执行res=(7+8)->\tres");
res = c1 = c2; //两次调用operator=(const myComplex &c)
c1.outCom("\t\t\tc1");
c2.outCom("\t\t\tc2");
res.outCom("执行res=c1=c2->\tres");
return 0;
}
关于重载赋值运算符应该注意以下两点。
1)赋值运算符必须重载为成员函数,不能重载为友元函数。
2)为了保持与通常意义下的赋值运算符的功能相一致,应该让重载的赋值运算符仍然能连续使用,即"res = c1 =c2;" 应成立。所以 operator= 函数通常要返回引用,具体到本例中,返回值类型是 myComplex &。
二、浅拷贝和深拷贝
1、浅拷贝
▶ 浅拷贝: 同类对象之间可以通过赋值运算符"="互相赋值。如果没有经过重载,“=” 的作用就是将赋值号右侧对象的值一一赋值给左侧的对象。这相当于值的拷贝,称为“浅拷贝”。
▶ 如果赋值的对象中涉及指针或是引用,则它们之间是互相关联的,一个值变化了,另一个值也跟着变化。因为对象中的指针指向的是同一个内存地址。
例: 浅拷贝的含义
#include <iostream>
using namespace std;
class pointer
{
public: //公有, main函数可以处理成员
int a;
int* p; //指向整型数的指针
pointer() //构造函数
{
a = 100; p = new int(10);
}
pointer(const pointer& tempp) //复制构造函数
{
if (this != &tempp) //避免 a = a 这样的赋值
{
a = tempp.a; p = tempp.p;
}
}
};
int main()
{
pointer p1; //使用默认构造函数
pointer p2(p1); //使用复制构造函数
pointer p3 = p1; //使用复制构造函数
cout << "\n初始化后,各对象的值及内存地址" << endl;
cout << "对象名\t对象地址 a的值 p中的值 p指向的值 p地址" << endl;
cout << "p1:\t" << &p1 << ", " << p1.a << ", " << p1.p << ", " << *p1.p << ", " << &p1.p << endl;
cout << "p2:\t" << &p2 << ", " << p2.a << ", " << p2.p << ", " << *p2.p << ", " << &p2.p << endl;
cout << "p3:\t" << &p3 << ", " << p3.a << ", " << p3.p << ", " << *p3.p << ", " << &p3.p << endl;
std::cout << "Hello World!\n";
}

int main()
{
pointer p1; //使用默认构造函数
pointer p2(p1); //使用复制构造函数
pointer p3 = p1; //使用复制构造函数
*p1.p = 20;
p2.a = 300;
cout << "\n修改后,各对象的值及内存地址" << endl;
cout << "对象名\t对象地址 a的值 p中的值 p指向的值 p地址" << endl;
cout << "p1:\t" << &p1 << ", " << p1.a << ", " << p1.p << ", " << *p1.p << ", " << &p1.p << endl;
cout << "p2:\t" << &p2 << ", " << p2.a << ", " << p2.p << ", " << *p2.p << ", " << &p2.p << endl;
cout << "p3:\t" << &p3 << ", " << p3.a << ", " << p3.p << ", " << *p3.p << ", " << &p3.p << endl;
std::cout << "Hello World!\n";
}

浅拷贝可能造成的问题
1)重复释放同一块空间产生错误
如:当对象pl消亡时,需要释放构造函数中new()动态申请的空间。而当对象P2消亡时,也会试图释放这个空间,造成重复释放同一块空间,程序出错。
2) 某块内存永远不会被释放而成为内存垃圾
pointer p4; //调用默认构造函数
p4 = p1;
创建对象p4时,为p4中的成员变量p分配了空间,并赋了初值10。执行语句"p4=p1;" p4中的成员变量p指向了p1中的指针指向的地址,而丢弃了原来指向的地址,这块内存成为内存垃圾。
-
深拷贝
深拷贝:重载赋值运算符后,赋值语句的功能是将一个对象中指针成员变量指向的内容复制到另一个对象中指针成员变量指向的地方,这样的拷贝叫“深拷贝”。
#include <iostream>
using namespace std;
class pointer
{
public: //仅有,main函数可以处理成员
int a;
int* p; //指向整型数据的指针
pointer() //构造函数
{
a = 100; p = new int(10);
}
pointer(const pointer& tempp) //复制构造函数
{
if (this != &tempp)
{
a = tempp.a, p = new int(); *p = *tempp.p;
}
}
~pointer()
{
if (p != NULL) delete p;
}
pointer& operator=(const pointer& c) //重载赋值运算符
{
if (this == &c) return *this; //避免a=a这样的赋值
delete this->p; //释放原来的空间
this->p = new int(*c.p); //申请新空间保存值
return *this;
}
};
int main()
{
pointer p1; //使用默认构造函数
pointer p2(p1); //使用复制构造函数
pointer p3;
p1 = p1; //赋值重载函数
p3 = p1; //赋值重载函数 operator=(const pointer &c)
cout << "\n初始化后,各对象的值及内存地址" << endl;
cout << "对象名 对象地址 a的值 \tp中的值 p指向的值 \tp地址" << endl;
cout << "p1:\t" << &p1 << ", " << p1.a << ", " << p1.p << ", " << *p1.p << ", " << &p1.p << endl;
cout << "p2:\t" << &p2 << ", " << p2.a << ", " << p2.p << ", " << *p2.p << ", " << &p2.p << endl;
cout << "p3:\t" << &p3 << ", " << p3.a << ", " << p3.p << ", " << *p3.p << ", " << &p3.p << endl;
return 0;
}

第三节 重载流插入运算符和流提取运算符
一、重载流插入运算符和流提取运算符
▶ "<<" 是流插入运算符, ">>" 流提取运算符
▶ 头文件 #include <iostream> 已经对这两个运算符进行了重载,用来输出C++基本数据类型。cout是ostream类的对象,cin是istream类的对象。
▶ 程序员可以对这两个运算符进行重载,使之用于自定义类的对象。但重载函数不能是流类库中的成员,而必须是重载为类的友元。
▶ 重载流插入运算符的一般格式如下:
ostream &operator<<(ostream & output, 类名 & 对象名)
{ ...../ /函数代码
return output;
}
● output 是类ostream对象的引用,它是cout的别名.
● 插入运算符函数不改变对象的值,所以第二个参数 “类名 & 对象名” 或 “类名 对象名” 两种重载方式都是可以的。
▶ 重载流提取运算符的一般格式
istream & operator>>(istream & input, 类名 & 对象名)
{ ........ //函数代码
return input;
}
● input是类istream对象有引用 ,它是cin的别名
● 提取运算符函数需要返回新的对象值,所以只能使用引用 ,即"类名 &对象名", 不能使用"类名 对象名"
例: 流插入运算符和流提取运算符重载为友元
#include <iostream> //输入输出流函数头文件
#include <string> //字符串处理函数头文件
#include <cstdlib> //常用函数库atof()就是其中的函数
using namespace std;
class myComplex //复数类
{
private:
double real, imag; //复数的实部,虚部
public:
myComplex() :real(0), imag(0) {} //构造函数
myComplex(double r, double i) :real(r), imag(i) {} //构造函数
//友元,插入,os是out别名
friend ostream& operator<<(ostream& os, const myComplex& c);
//友元,提取,is是 cin 别名
friend istream& operator>>(istream& is, myComplex& c);
};
ostream& operator<<(ostream& os, const myComplex& c)
{
if (c.imag >= 0)
{
os << c.real << "+" << c.imag << "i"; //以a+bi的形式输出
}
else
{
os << c.real << c.imag << "i"; //以a-bi的形式输出
}
return os;
}
istream& operator>>(istream& is, myComplex& c)
{
string s;
is >> s; //将a+bi作为字符串读入,a+bi中间不能有空格
int pos = s.find("+", 0); /*从字符吕s下标o开始,查找"+",返回“+”在s中的下标, 没找到,返回-1*/
if (pos == -1)
{
/*虚部为负数时,从字符串s下标1开始查找"-", 返回"-"在s中的下标 */
pos = s.find("-", 1);
}
string sReal = s.substr(0, pos); /*从0到+或-符号之间的字符取出,即分离出代表实部的字符串*/
/*c_str()函数返回一个指向字符串的指针,atof能将参数内容转换成浮点数*/
c.real = atof(sReal.c_str());
sReal = s.substr(pos, s.length() - pos - 1); //分离出代表虚部的字符串
c.imag = atof(sReal.c_str());
return is;
}
int main()
{
myComplex c, c1;
int n;
cout << "请输入两个复数([-]a±bi)和一个整数,以空格分"<<endl;
/*理解为oprator>>(cin.c)输入n的时候不需要重载*/
cin >> c >> c1 >> n;
cout << c << "," << n << "," << c1; //输出
return 0;
}
第四节 重载强制类型转换运算符
▶ 强制类型类型转换运算符是单目运算符,只能重载为成员函数,不能重载为全局函数。
▶ (类型名) 对象 ” 这个对对象进行强制类型转换的表达式就等价于 “对象.operator类型名()”, 即变成对运算符函数的调用。
#include <iostream>
using namespace std;
class myComplex
{
double real, imag; //私有成员
public:
myComplex(double r = 0, double i = 0) :real(r), imag(i) {}; //构造函数
/*重载强制类型转换运算答double, 程序中凡是遇到myComplex对象,都强制转换为double型,值为real*/
operator double() { return real; }
};
int main()
{
myComplex c(1.2, -3.4);
/*(double)c 等价于c.operator double(), 将c强制转换为double型,可以直接写成cout<<c, 输出1.2*/
cout << (double)c << endl;
double n = 12 + c; //等价于 double 12 + c.operatordouble()
cout << n;
return 0;
}
第五节 重载自增、自减运算符
▶ 自增运算符“++” 和自减 "--"运算符都可以被重载,有前置和后置之分。
例:自增运算符重载为成员函数,自减运算符重载为友元
#include <iostream>
using namespace std;
class CDemo
{
private:
int n;
public:
CDemo(int i = 0) :n(i) {}; //构造函数
CDemo& operator++(); //用于前置形式
CDemo operator++(int); //用于后置形式,多一个参数
//重载强制类型转换,凡是遇到CDemo对象就会调用该函数,强制转换成整型。
operator int() { return n; }
friend CDemo& operator--(CDemo&);
friend CDemo operator--(CDemo &, int);
};
CDemo& CDemo::operator++() //前置++
{
n++; return *this;
}
CDemo CDemo::operator++(int k) //后置++, 多一个参数
{
CDemo tmp(*this); //记录修改前对象
n++;
return tmp; //返回修改前的对象
}
CDemo & operator--(CDemo & d) //前置--
{
d.n--; return d;
}
CDemo operator--(CDemo& d, int) //后置--, 多一个参数
{
CDemo tmp(d); //记录修改前的对象
d.n--;
return tmp; //返回修改前的对象
}
int main()
{
CDemo d(10);
cout << (d++) << "."; //d先转换为整型,然后"d.operator++(0);" 输出10
cout << d << ","; //输出11
cout << (++d) << ","; //等价于"d.operator++();" 输出12
cout << d << ","; //输出12
cout << (d--) << ","; //等价于"d.operator--(d, 0); " 输出12
cout << d << ","; //输出11
cout << (--d) << ","; //等价于"d.operator--(); " 输出10
cout << d << endl; //输出10
return 0;
} //注意:有重载强制类型转换符,这里以整数型输出
第五章 类的继承与派生
第一节 类的继承与类的派生
一、继承的概念
▶ 类的派生是指从一个或多个以前定义的类产生新类的过程,原有的类称为基类,新产生的类称为派生类,基类所有的数据成员和成员函数。
▶ 派生类继承了基类的除构造函数和析构函数外的所有成员变量和成员函数。
▶ 派生类需要定义自己的构造函数和析构函数。
▶ 相对基类,派生类可以有以下变化:增加新的成员; 重新定义已有的成员函数;改变基类成员的访问权限。
▶ 派生类中定义了基类中同名的成员,则派生类的成员覆盖基类的同名成员。
二、派生类的定义与大小
1.派生类的定义
class 派生类名: 继承方式说明符 基类名
{ 类体
};
继承方式说明符是指如何控制基类成员在派生类中的访问属性,通常是public, 很少用protected和private。
例: 派生类改变基类成员的访问权限
#include <iostream>
using namespace std;
class BaseClass
{
public:
int v1, v2; //公有访问权限
BaseClass() :v1(1), v2(1) {} //构造函数
int temp1() {} //公有访问权限
};
class DerivedClass :public BaseClass
{
int v1; //改为私有访问权限
int temp1() {} //改为私有访问权限
public:
DerivedClass() : v1(10) {} //构造函数 初始化 v1=10
/*在派生类的成员函数中访问基类的成员时,需要使用类名加以限定,
如“Baseclass::v1”。 输出Base.v1=1*/
void printv()
{
cout << "v1=" << v1 << endl; //出派生类中的值v1=10;
cout << "Base.v1=" << BaseClass::v1 << endl;
}
};
int main()
{
BaseClass be;
DerivedClass dc;
dc.printv(); //调用派生类中的函数printv()
return 0;
}
-
类的大小
▶ 派生类对象占用的存储空间大小,等于基类成员变量占用的存储空间大小加上派生类对象自身成员变量占用的存储空间大小,与成员函数和类中的静态成员变量无关。
▶ 为类对象分配空间时, 遵循字节对齐的原则。
▶ 空类的大小是1
#include <iostream> using namespace std; class BaseClass //基类 { int v1, v2; //每个int变量占用4个字节 char v4; //每个char变量占1个字节 public: int temp1() {} }; class DerivedClass : public BaseClass //派生类 { int v3; int* p; //64位系统中,每个指针变量占8个字节 public: int temp() {} }; int main() { //输出Base=4+4+(1+3)(边界对齐)=12 cout << "Base=" << sizeof(BaseClass) << endl; //输出Derived=12(基类大小)+4+8=24 cout << "Derived=" << sizeof(DerivedClass) << endl; return 0; }
三、继承关系的特殊性
▶ 基类的友元不一定是派生类的友元;基类的成员函数是某类的友函数,则其作为派生类继承的成员函数仍是某类的友元函数。
例: 派生类继承了友元函数
#include <iostream>
using namespace std;
class another; //前向引用声明
class Base //基类
{
private:
float x;
public:
void print(const another& K);
};
class Derived : public Base //派生类
{
private:
float y;
};
class another //其他类
{
private:
int aaa;
public:
another() //构造函数
{
aaa = 100;
}
//基类的成员函数声明为本类的友元
friend void Base::print(const another& K);
};
void Base::print(const another& K)
{
cout << "Base:" << K.aaa << endl; //可以访问私有成员变量
}
int main()
{
Base a;
Derived d;
another ano; //aaa 初始化为100
a.print(ano); //输出: Base:100
d.print(ano); //输出:Base:100, 类Derived是类Base的派生类,
//它从类Base继承了print()函数。通过Derived的对象也能调用print.
// 函数访问another的私有成员aaa
return 0;
}
▶ 基类中的成员是静态的,则在其派生类,被继承的成员也是静态的。无论有多少个对象被创建,这些成员都只有一个拷贝,它为基类和派生类的所有对象所共享。
▶ 派生类访问这些成员时,通常用<类名>::<成员名>的方式引用或调用。
例: 派生关系中的静态成员
#include <iostream>
using namespace std;
class Base //基类
{
private:
float x;
public:
static int staV; //静态成员
Base() //无参构造函数
{
staV++;
}
};
int Base::staV = 0; //公有成员
class Derived :public Base //派生类
{
private:
float y;
public:
Derived() //无参构造函数
{
staV++;
}
};
int main()
{
Base a; //创建基类对象, 调用一次基类构造函数staV的值
cout << a.staV << endl; //输出1
Derived d; /*创建派生类对象,先调用一次基类的无参构造函数,又调用一次派生类的无参构造函数,staV的值变为3,*/
cout << d.staV << endl; //输出3
return 0;
}
四、有继承关系的类之间的访问
▶ 派生类中的成员函数可以访问基类中的公有成员,但不能直接访问基类中的私有成员。
▶ 基类的成员和派生类新培的成员同名时,派生类成员优先,此时要是用基类同名成员,需要用<基类名>::<成员名>的方式引用或调用。
例: 访问基类和派生类成员的方式
#include <iostream>
using namespace std;
class CB //基类
{
public:
int a;
CB(int x) //构造函数
{
a = x;
}
void showa() //成员函数
{
cout << "Class CB--a = " << a << endl;
}
};
class CD :public CB
{
public:
int a; //与基类同的成员变量
CD(int x, int y) :CB(x) { a = y; } //构造函数CB::a=x.CD::a=y
void showa() //与基类同名的成员函数
{
cout << "Class CD--a=" << a << endl;
}
void print2a()
{
cout << "a=" << a << endl; //访问派生类a
cout << "CB::a = " << CB::a << endl; //访问基类a
}
};
int main()
{
CB CBobj(12); //创建基类CB对象
CBobj.showa(); //输出 class CB--a=12
CD CDobj(48, 999); //创建派生类CD对象
CDobj.showa(); //访问派生类的showa() class CD--a=999
CDobj.CB::showa(); //访问基类的成员函数showa(); Class CB--a=48
cout << "CDobj.a=" << CDobj.a << endl;
cout << "CDobj.CB::a=" << CDobj.CB::a << endl; //CDobj.CB::a=48
}
五、protected访问范围说明符
▶ 派生类可以直接访问基类中的保护成员
▶ 派生类不能直接访问基类中成员变量是私有成员。
▶ 将基类部分成员设置为保护成员,即能起到隐藏的目的,又避免了派生类成员函数要访问它们时只能间接访问所带来的麻烦。
▶ 派生类的成员函数只能访问所作用的那个(即this指针指向的对象)的基类保护成员,不能访问其他基类对象的基类保护成员。
例: 基类中保护成员的访问方式
#include <iostream>
using namespace std;
class BaseClass //基类
{
protected:
int v1, v2; //保护成员
public:
void SetValue(int m, int n)
{
v1 = m, v2 = n; //访问本类的成员变量
}
void PrintValue(); //基类成员函数
};
void BaseClass::PrintValue()
{
cout << "v1=" << v1 << "\tv2=" << v2; //直接访问
}
class DerivedClass : public BaseClass //派生类
{
int v3; //派生类自己的成员变量
public:
void SetValue(int m, int n, int k)
{
v1 = m; v2 = n; //派生类中可以访问基类中的保护成员变量
v3 = k; //本类中的成员变量
}
void SetValue(int m, int n)
{
v1 = m; v2 = n; //派生类中可以访问基类中的保护成员变量
}
void PrintValue();
};
void DerivedClass::PrintValue() //派生类成员函数
{
cout << endl;
BaseClass::PrintValue();
cout << "\tv3=" << v3 << endl;
}
int main()
{
BaseClass baseCla;
DerivedClass derivedCla;
cout << "初始时的随机值: " << endl;
baseCla.PrintValue();
derivedCla.PrintValue();
cout << "派生类修改从基类继承的值后: " << endl;
derivedCla.SetValue(10, 20);
baseCla.PrintValue();
derivedCla.PrintValue();
cout << "从派生类修改从基类继承的值及本类的值:" << endl;
derivedCla.SetValue(100, 200, 300);
baseCla.PrintValue();
derivedCla.PrintValue();
cout << "从基类修改和派生修改" << endl;
baseCla.SetValue(50, 60);
derivedCla.SetValue(400, 500, 600);
baseCla.PrintValue();
derivedCla.PrintValue();
return 0;
}
六、多重继承
▶ 多重继承: 一个派生类可以同时有多个基类。
▶ 多重继承形式为:
class 派生类名:继承方式说明符 基类名1, 继承方式说明符 基类名2,..., 继承方式说明符 基类名n { 实体 }
例: 多重继承下的访问方式
#include <iostream>
using namespace std;
class CB1
{
public:
int a; //重名
CB1(int x) //构造函数
{
a = x;
}
void showa() //重名
{
cout << "Class CB1 ==> a = " << a << endl;
}
};
class CB2
{
public:
int a; //重名
CB2(int x) //构造函数
{
a = x;
}
void showa() //重名
{
cout << "Class CB2 ==> a = " << a << endl;
}
};
class CD :public CB1, public CB2 //多重继承, 两个基类
{
public:
int a; //与两个基类成员变量a重名
CD(int x, int y, int z) :CB1(x), CB2(y)
{
a = z; //构造函数
}
void showa() // 与两个基类成员函数showa()重名
{
cout << "Class CD ==> a = " << a << endl;
}
void print3a() //分别输出派生类的a及其两个基类的重名成员a
{
cout << "a = " << a << endl;
cout << "CB1::a=" << CB1::a << endl;
cout << "CB2::a=" << CB2::a << endl;
}
};
int main()
{
CB1 CB1obj(11);
CB1obj.showa();
CD CDobj(101, 202, 909);
CDobj.showa(); //调用派生类showa()
CDobj.CB1::showa(); //调用基类showa();
cout << "CDobj.a=" << CDobj.a << endl; //访问派生类成员
cout << "CDobj.CB2::a=" << CDobj.CB2::a << endl; //访问基类CB2的成员a
return 0;
}
多重继承需要注意的地方:
▶ 二义性问题:多重继承时,如果多个基类中有重名的成员,则它们都被继承到派生类中。在这种情况下,访问重名成员时会遇到二义性问题。
▶ 避免二义性方法, 当要访问派生类对象中的某个变量时,添加“基类::”作为前缀,指明需要访问从哪个基类继承来的,从而可以排队二义性。
▶ 如果基类中的成员均不同名,则派生类对象访问它们时不会出现二义性。
例: 多重继承
#include <iostream>
using namespace std;
class BaseClass1
{
public:
int v1, v2; //v1重名
BaseClass1(); //声明构造函数
BaseClass1(int, int); //声明2个参数的构造函数
~BaseClass1(); //声明析构函数
};
BaseClass1::BaseClass1()
{
cout << "BaseClass1 无参构造函数" << endl;
}
BaseClass1::BaseClass1(int m, int n) :v1(m), v2(n)
{
cout << "BaseClass1 带2个参数构造函数" << endl;
}
BaseClass1::~BaseClass1()
{
cout << "BaseClass1 析构函数" << endl;
}
class BaseClass2 //定义基类2
{
public:
int v1, v4; //v1重名
BaseClass2();
BaseClass2(int, int);
~BaseClass2();
};
BaseClass2::BaseClass2()
{
cout << "BaseClass2 无参构造函数 " << endl;
}
BaseClass2::BaseClass2(int m, int n) :v1(m), v4(n)
{
cout << "BaseClass2 带2个参数构造函数" << endl;
}
BaseClass2::~BaseClass2()
{
cout << "BaseClass2 析构函数" << endl;
}
class DerivedClass :public BaseClass1, public BaseClass2
{
public:
int v3;
public:
DerivedClass();
DerivedClass(int);
DerivedClass(int, int, int, int);
~DerivedClass();
};
DerivedClass::DerivedClass()
{
cout << "DerivedClass 无参构造函数" << endl;
}
DerivedClass::DerivedClass(int k) : v3(k)
{
cout << "DerivedClass 带1个参数构造函数" << endl;
}
DerivedClass::DerivedClass(int m, int n, int k, int t):BaseClass1(m, n), BaseClass2(n, t), v3(k)
{
cout << "DerivedClass 带4个参数构造函数" << endl;
}
DerivedClass::~DerivedClass()
{
cout << "DerivedClass 析构函数" << endl;
}
int main()
{
cout << "带参数对象的创建" << endl;
DerivedClass derivedCla1(1000, 2000, 3000, 4000);
cout << "v1=" << derivedCla1.BaseClass1::v1 << endl;
cout << "v1=" << derivedCla1.BaseClass2::v1 << endl;
//cout<<"v1="<<derivedCla1.v1<<endl; 出错,二义性
cout << "v2=" << derivedCla1.v2 << endl;
cout << "v4=" << derivedCla1.v4 << endl;
cout << "v3=" << derivedCla1.v3 << endl;
return 0;
}
第二节 访问控制
一、公有继承

二、类型兼容规则
▶ 在公有派生的情况下,有以下3条类型兼容规则。
1) 派生类的对象可以赋值给基类对象
2) 派生类对象可以用来初始化基类折腾、
3)派生类的指针可以赋值给基类的指针。
上述3条规则反过来是不成立的。
例: 验证使用类型兼容规则的输出结果
#include <iostream>
using namespace std;
class A //定义基类
{
int an;
public:
A() {}
A(int n)
{
an = n;
}
void print()
{
cout << "A的对象:";
cout << "an: " << an;
}
void print(int k) //不同的输出格式
{
cout << "an: " << an;
}
};
class B : public A //公有派生类
{
int bn;
public:
B(int n) :A(2 * n) //类B的构造函数
{
bn = n;
}
void print()
{
cout << "\nB的对象";
A::print(1); //调用A类的void print(int k)函数
cout << ", bn = " << bn << endl;
}
};
int main()
{
A a(10);
B b(20); //调用构造函数初始化
a.print(); //调用类A中的print()函数
b.print(); //调用类A中的print()函数
a = b; /*派生类对象赋值给基类对象,将派生类对象中的基类对象逐个字节地复制到左边的基类对象中*/
a.print();
b.print();
return 0;
}
类型兼容规则还允许下列赋值规则
A &r = b; //派生类对象初始化基类引用
A *pa = &b; //派生类对象地址赋值给基类指针
B *pb = &b;
pa=pb; //派生类指针赋值给基类指针
三、私有继承
私有继承的访问控制

四、保护继承
▶ 保护继承中,基类的公有成员和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可以直接访问。
▶ 派生类的其他成员可以直接访问从基类继承来的公有和保护成员,但在类外通过派生类的对象无法直接访问它们。
第三节 派生类的构造函数和析构函数
一、派生类的构造函数与析构函数
▶ 派生类并不继承基类的构造函数,所以需要在派生类的构造函数中调用基类的构造函数,以完成对从基类继承的成员变量的初始化工作。
▶ 定义派生类构造函数的一般格式:
派生类名::派生类名(参数表):基类名1(基类1初始化参数表), ..., 基类名m(基类m初始化参数表),成员对象名1(成员对象1 初始化参数表), ...,成员对象名n(成员对象n 初始化参数表)
{ 派生类构造函数函数体 //其他初始化操作
}
例:
DerivedClass::DerivedClass(int m, int n, int k, int t):BaseClass1(m, n),BaseClass(n,t),v3(k)
{
cout<<"DerivedClass 带4个参数的构造函数"<<endl;
}
派生类构造函数执行的一般次序如下:
1) 调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)。
2) 对派生类新增的成员变量初始化,调用顺序按照它们在类中声明的顺序。
3) 执行派生类的构造函数体中的内容。
例:调用基类和派生类的构造函数、析构函数和成员函数
#include <iostream>
using namespace std;
class Base
{
private:
int Y;
public:
Base(int y = 0) //构造函数
{
Y = y;
cout << "Base(" << y << ")" << endl;
}
~Base() //析构函数
{
cout << "~Base()" << endl;
}
void print() //成员函数
{
cout << Y << " ";
}
};
class Derived :public Base //派生类
{
private:
int Z;
public:
Derived(int y, int z) :Base(y) //构造函数
{
Z = z;
cout << "Derived(" << y << ", " << z << ")" << endl;
}
~Derived() //析构函数
{
cout << "~Derived()" << endl;
}
void print() //成员函数
{
Base::print();
cout << Z << endl;
}
};
int main()
{
Derived d(10, 20); //创建派生类对象并调用构造函数
d.print(); //调用成员函数
return 0; //最后执行析构函数,注意顺序先构造后析构
}
二、复制构造函数
▶ 对于一个类,如果程序中没有定义复制构造函数,则编译器会自动生成一个隐含的复制构造函数,这个隐含的复制构造函数会自动调用基类的复制构造函数,对派生类新增的成员对象一一执行复制。
例:派生类中的复制构造函数
#include <iostream>
using namespace std;
class A //基类
{
public:
A() //无参构造函数
{
i = 100;
cout << "类A无参构造函数" << endl;
}
A(const A& s) //复制构造函数
{
i = s.i;
cout << "类A复制构造函数" << endl;
}
private:
int i;
};
class B :public A //公有派生类
{
private:
int f;
public:
B() //无参构造函数
{
f= 20;
cout << "类B无参构造函数" << endl;
}
B(const B& v) :A::A(v), f(v.f) //复制构造函数
{
cout << "类B复制构造函数" << endl;
}
};
int main()
{
A a;
B b;
B bb(b);
return 0;
}
第四节 类之间的关系
一、类与类之间的关系
▶ 使用已有类编写新的类有两种方式:继承和组合
▶ 类和类之间的两种基本关系: 继承关系和组合关系(包含关系)。
▶ 继承关系也称为"is a"关系或“是”关系。是指在公胡继承的赋值兼容规则下,如果类B公有继承于类A,也可以使用类A的对象的任何地方,则类B的对象同样也能使用,即每一个类B的对象,“就是一个”类A的对象,但反之则不然,即如果需要一个类B的对象,则类A的对象就不行。
▶ 组合关系也称为"has a"关系或“有”关系,表现为封闭类,即一个类以另一个类的对象作为成员变量。
二、封闭类的派生
▶ 封闭类: 一个类的成员变量是另一个类的对象。
▶ 定义封闭类构造函数的一般形式:
类名;:类名(形参表):内嵌对象1(形参表), 内嵌对象2(形参表), ...
{ 类体
}
其中, "内嵌对象1(形参表), 内嵌对象2(形参表),... "是初始化列表,其作用是对内嵌对象进行初始化。
▶ 在派生类也是封闭类的情况下,调用构造函数的顺序是:是先根据派生层次从上到下依次执行所有基类的构造函数,最后执行自身的构造函数。如果某个类是封闭类,则在执行本类构造函数之前,先按照成员对象的定义顺序执行各个成员对象所属类的构造函数。
▶ 而当派生类对象消亡时,执行析构函数的次序与执行构造函数的次序相反。
第五节 多层次的派生
▶ 派生 可以是多层次的: 类A派生类B,类B可以再派生类C,类C又能够派生类D,以此类推。在这种情况下,称类A是类B的直接基类,类B是类C的直接基类,类A是类C的间接基类。当然,类A也是类D的间接基类。在定义派生类时,只需写直接基类,不需写间接基类。派生类沿着类的层次自动向上继承它所有的直接和间接基类的成员。
▶ 在c++中,类之间的继承关系具有传递性。
▶ 派生类的成员包括派生 类自己定义的成员、直接基类中定义的成员及所有间接基类中定义的全部成员。
▶ 当生成派生类的对象时,会从最顶层的基类开始逐层往下执行所有基类的构造函数,最后执行派生类自身的构造函数;当派生类对象消亡时,会先执行自身的析构函数,然后自底向上依次执行各个基类的析构函数。
第六节 基类与派生类指针的互相转换
▶ 在公有派生的情况下,有以下3条类型兼容规则
1) 派生类的对象可以赋值给基类对象。
2) 派生类对象可以用来初始化基类引用。
3) 派生类的指针可以赋值给基类的指针,即基类指针指向派生类对象。
▶ 基类的指针不参直接赋值给派生类的指针。但是通过强制类型转换,可以将基类指针强制转换成派生类指针后再赋值给派生类指针。
▶ 即使基类指针指向一个派生类的对象,也不能通过基类指针访问基类中没有而仅在派生类中定义的成员函数。
▶当使用指针调用类中的函数时,需要根据指针的类型来决定可调用的函数。
▶如果一个指针是基类类型的,则不管它指向的是基类对象还是派生类对象,都仅能调用基类中声明的函数,而不能调用基类中没有声明而仅在派生类中声明 的函数。
▶ 如果一个指针是派生类类型的,则调用的是派生类中的函数; 如果派生类中没有声明,则调用从基类继承的同名函数。
第六章 多态与虚函数
第一节 多态的基本概念
一、多态
▶ 面向对象程序设计语言有封装、继承和多态3种机制,运用这3种机制能有效提高程序的可读性、可扩充性和可重用性。
▶ 多态: 不同对象可以调用相同名称的函数,但可导致完全不同的行为的现象。
▶ 封装可以使得代码模块化,继承可以让程序在原有的代码基础上进行扩展,它们的目的都是为了代码复用。
▶ 多态是为了接口复用,不论传递过来的是哪个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。
▶ 多态分为编译时多态和运行时多态。
▶ 编译时多态(静态多态):在编译阶段就能绑定调用语句与调用函数入口地址,即函数的重载(包括运算符的重载).
▶ 运行时的多态(动态多态):函数调用与代码入口地址的绑定需要在运行时刻才能确定,这也称为动态联编或动态绑定。
▶ 运行时多态通过基类指针或基类引用调用虚函数。
二、虚函数
▶ 虚函数:在类的定义中使用 virtual 关键字来限定的成员函数即成为虚函数。
▶ virtual 关键字只在类定义中的成员函数声明处使用,不能在类外部写成员函数体时使用。
▶ 定义虚函数的一般格式:
例: class A
{
public:
virtual void fun(); //声明虚函数
}
void A::fun() {........} //定义虚函数
关于虚函数,有以下几点需要注意。
1)虚函数一般不声明为内联函数。虽然将虚函数声明为内联函数不会引起错误,但因为内联函数是在编译阶段进行静态处理的,而对虚函数的调用是动态绑定的。
2) 构造函数不能声明为虚函数。构造函数一般用来初始化对象,只有在一个对象生成之后,才能发挥多态作用。如果将构造函数声明为虚函数,则表现为在对象还没有生成的时候来定义它的多态,这两占是不统一的。另外,构造函数不能被继承,因而不能声明为虚函数。
3)静态成员函数不能声明为虚函数。静态成员函数对于每个类来说只有一份代码,所有的对象都共享这份代码,它不归某个对象所有,所以也没有动态绑定的必要性。
4)友元函数不能声明为虚函数。友元函数不属于类的成员函数,不能被继承,没有声明为虚函数的必要。
5)全局函数不能声明为虚函数。虚函数是为了与继承机制配合实现多态的,而全局函数(非成员函数)不属于某个类,没有继承关系,只能被重载,不能被覆盖,声明为虚函数也起不到多态的作用。因此编译器会在编译时绑定全局函数。
6)派生类重写基类的虚函数实现多态,要求函数名、参数列表及返回值类型要完全相同。
7)基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
8)不要在构造函数和析构函数中调用虚函数。在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。
9)最好将基类的析构函数声明为虚函数。
三、通过基类指针实现多态
例:通过基类指针实现多态示例
#include <iostream>
using namespace std;
class A
{
public:
virtual void Print() //虚函数
{
cout << "A::Print" << endl;
}
};
class B :public A //公有继承
{
public:
virtual void Print() //虚函数
{
cout << "B::Print" << endl;
}
};
class D :public A //公有继承
{
public:
virtual void Print() //虚函数
{
cout << "D::Print" << endl;
}
};
class E :public B //公有继承
{
public:
virtual void Print() //虚函数
{
cout << "E::Print" << endl;
}
};
int main()
{
A a; B b; D d; E e;
A* pa = &a; //基类指针pa指向基类对象a
B* pb = &b; //派生类指针pb指向派生类对象b
pa->Print(); //多态, 目前指向基类对象,调用a.Print(), 输出 A::Print
pa = pb; //基类指针pa指向派生类对象b
pa->Print(); //多态, 目前指向派生类对象,调用b.Print().输出B::Print
pa = &d; //基类指针pa指向派生类对象d
pa->Print(); //多态, 目前指向派生类对象,调用d.Print().输出D::Print
pa = &e; //基类指针pa指向派生类对象e
pa->Print(); //多态, 目前指向派生类对象,调用e.Print().输出E::Print
return 0;
}
多态:同一条函数调用语句能调用不同的函数体。
注意:程序中去掉关键字virtual, 则程序的输出为A::Print.
四、通过基类引用实现多态
▶ 通过基类的引用调用基类和派生类中同名、同参数表的虚函数时,若其引用的是一个基类的对象,则调用的是基类的虚函数,若其引用的是一个派生类的对象,则调用的是派生类的虚函数。
例:基类引用实现多态
#include <iostream>
using namespace std;
class A
{
public:
virtual void Print() //虚函数
{
cout << "A::Print" << endl;
}
};
class B :public A //公有继承
{
public:
virtual void Print() //虚函数
{
cout << "B::Print" << endl;
}
};
void PrintInfo(A& r) //定义全局函数
{
r.Print(); /*多态,使用基类引用调用哪个Print()取决于r引用了哪个类的对象,
调用时实参为基类对象,
则调用函数A::Print();如果实参是派生类对象,则调用函数B::Print().*/
}
int main() {
A a;
B b;
PrintInfo(a); //使用基类对象,调用A::Print()
PrintInfo(b); //使用派生类对象,调用B::Print()
return 0;
}
第二节 多态的实例
▶ 在面向对象的程序设计中,使用多态能够增加程序的可扩充性,此外,使用多态也能起到精简代码的作用。
例:
第三节 多态的使用
▶ 通过基类的指针可以调用虚函数实现多态
▶ 通过基类的引用可以调用虚函数实现多态。
▶ 通过普通成员函数(静态成员函数、构造函数和析构函数除外)中调用其他虚成员函数,并且是多态的。
▶ 可以在构造函数和析构函数中调用虚函数。但这样调用的虚函数不是多态的。
例:在成员函数中调用虚函数
#include <iostream>
using namespace std;
class CBase //基类
{
public:
void func1() //不是虚函数
{
cout << "CBase::func1()" << endl;
func2();
func3();
}
virtual void func2() //虚函数
{
cout << "CBase::func2()" << endl;
}
void func3() //不是虚函数
{
cout << "CBase::func3()" << endl;
}
};
class CDerived : public CBase //派生类
{
public:
virtual void func2() //虚函数
{
cout << "CDerived::func2()" << endl;
}
void func3() //不是虚函数
{
cout << "CDerived::func3()" << endl;
}
};
int main()
{
CDerived d;
CBase a;
a.func1();
cout << endl;
d.func1();
return 0;
}
注意d.func1()的执行过程:
▶ 派生类中没有定义func1()函数,所以执行基类func1()函数,输出CBase::func1();
▶ 在func1()中调用func2(), 相当于this->func2(), 通过基类指针this调用func2()是虚函数,所以是多态的,由于d是派生类,所以最终调用派生类func2(),输出CDerived::func2();
▶ 最后在func1()中调用func3(), 不是虚函数,执行基类的func3(). 输出出CBase::func3()
例: 多态与非多态的对比
#include <iostream>
using namespace std;
class A
{
public:
void func1()
{
cout << "A::func1" << endl;
}
virtual void func2() //虚函数
{
cout << "A::func2" << endl;
}
};
class B :public A
{
public:
virtual void func1() //虚函数
{
cout << "B::func1" << endl;
}
void func2() //A类中func2()是虚函数, 这里自动国万米虚函数
{
cout << "B::func2" << endl;
}
};
class C :public B
{
public:
void func1() //B类中func1()是虚函数,这里自动成为虚函数
{
cout << "C::func1" << endl;
}
void func2() //A类中func2()是虚函数,这里自动成为虚函数
{
cout << "C::func2" << endl;
}
};
int main()
{
C obj;
A* pa = &obj; //基类指针指向派生类对象
B* pb = &obj; //基类指针指向派生类对象
pa->func2(); //满足多态: pa是基类指针+基类A的func2()是虚函数
pa->func1(); //不是多态
pb->func1(); //满足多态;pb是基类指针 + 基类B中func1()是虚函数
return 0;
}
注意:
▶ 实现多态时,必须满足的条件是:使用基类指针或引用来调用基类中声明的虚函数。掌握了这一点,容易区分多态与非多态。
▶ 派生类中继承自基类的虚函数,可以写virtual关键字,也可以省略这个关键字,这不影响派生类中的函数也是虚函数。
第四节 虚析构函数
▶ C++不支持虚构造函数,但支持虚析构函数
▶ 虚析构函数没有参数,没有返回值类型,声明虚析构函数的一般格式: virtual ~类名()
▶ 只要基类的析构函数被说明为虚函数,则派生类的析构函数,无论是否使用virtual说明,都自动成为虚函数。
▶ 使用虚析构函数的目的是为了在对象消亡时实现多态。具体来说,设置了虚析构函数后,在使用指针或引用时可以动态绑定,实现运行时的多态,保证使用基类类型的指针能够调用适当的析构函数针对不同的对象进行清理工作,以避免造成内存泄漏。
例: 不使用虚析构函数的情况
#include <iostream>
using namespace std;
class ABase
{
public:
ABase() //构造函数
{
cout << "ABase 构造函数" << endl;
}
~ABase() //析构函数
{
cout << "ABase::析构函数" << endl;
}
};
class Derived :public ABase //公有继承
{
public:
int w, h; //两个成员变量
Derived()
{
cout << "Derived 构造函数" << endl;
w = 4; h = 7;
}
~Derived() //析构函数
{
cout << "Derived::析构函数" << endl;
}
};
int main()
{
ABase* p = new Derived(); //使用基类指针指向用new创建的派生类对象
delete p;
return 0;
}
注意:使用基类指针指向使用new创建的派生类对象,在对象消亡时,仅调用了基类的析构函数,派生类对象占用的空间并没有完全释放,造成了内存泄漏。
▶ 在这种情况下,C++规定,需要将基类的析构函数声明虚析构函数。派生类的析构函数都自动成为虚析构函数。
virtual ~ABase()
{
cout<<"ABase::析构函数"<<endl;
}
▶ 一个类如果定义了虚函数,则最好将析构函数也定义成虚函数。不过切记,构造函数不是虚函数。
第五节 纯虚函数和抽象类
一、纯虚函数
▶ 纯虚函数是声明在基类中的虚函数,没有具体的定义,而由各派生类根据实际需要给出各自的定义。纯虚函数只有函数的名字但不具备函数的功能,不能调用基类中的这个函数。
▶ 说明纯虚函数的一般形式为; virtual 函数类型 函数名(参数列表) = 0
注意:必须有=0, 不能有没有函数体,大括号也不能有。
二、抽象类
▶ 一个类可说明多个纯虚函数,包含纯虚函数的类称为抽像类。
▶ 抽象类的派生类中,如果没有给出全部纯虚函数的定义,则派生类继续是抽象类。
▶ 一个抽象类只能作为基类来派生新类,不能创建抽象类的对象,即抽象类不能实例化一个对象。
▶ 可以定义抽象类的指针和引用。这样的指针和引用可以指向并访问派生类的成员 ,这种访问具有多态性。
▶ 抽象类至少含有一个虚函数,而且至少有一个虚函数 是纯虚函数,以便将它与空的虚函数区分开来,如下:
virtual void area() = 0; //纯虚函数
virtual void area() {} ; //空的虚函数
例:抽象类示例
#include <iostream>
using namespace std;
class A
{
private:
int a;
public:
virtual void print() = 0; //纯虚函数
void funcl() //成员函数
{
cout << "func1" << endl;
}
};
class B :public A
{
public:
void print(); //声明成员函数
void funcl() //声明成员函数
{
cout << "B_func1" << endl;
}
};
void B::print() //具体的定义基类中的纯虚函数
{
cout << "B_print" << endl;
}
int main()
{
//A a; //错误,抽象类不能实例化
//A *p = new A; //错误,不能创建类A的实例
//A b[2]; //错误,不能声明抽象类的数组
A* pa; //正确,可以声明抽象类的指针
A* pb = new B; //正确,使用基类指针指向派生类对象
pb->print(); //调用的是类B中的函数,多态,输出B_print
B b;
A* pc = &b;
pc->funcl(); //不是虚函数,调用的是类A中的函数,输出func1
return 0;
}
三、虚基类
▶ 多重继承有天生的二义性,通过''类名::“排除二义性。但有些情况下,即使添加"类名::", 也没有办法排除二义性。
▶ 为了避免产生二义性,C++提供虚基类机制,使得在派生类中,继承同一个间接基类的成员仅保留一个版本。
▶ 定义虚基类的一般格式如下:
class 派生类名:virtual 派生方式 基类名 {派生类体}
上图所示的各类的继承关系如下:
class A
class B : virtual public A
class C : virtual public A
class D : public B, public C
例:虚基类
#include <iostream>
using namespace std;
class A
{
public:
int a;
void showa()
{
cout << "a=" << a << endl;
}
};
class B :virtual public A //对类A进行了虚继承
{
public:
int b;
};
class C :virtual public A //对类A进行了虚继承
{
public:
int c;
};
class D :public B, public C
{
//派生类D的两个基类B,C具有共同的基类A
//采用了虚继承,从而使类D的对象中只包含着类A的1个实例
public:
int d;
};
int main()
{
D Dobj; //说明派生类D的对象
Dobj.a = 11; //若不是虚继承,此行会出错! 因为"D::a"具有二义性
Dobj.b = 22;
Dobj.showa(); //输出 a = 11 若不是虚继承,此行会出错,因为"D::showa()"具有二义性
cout << "Dobj.b=" << Dobj.b << endl;
}
第七章 输入/输出流
第一节 流类简介
▶ C++中没有输入/输出语句,输入和输出通过流完成
▶ 在C++的标准类库中,将与数据输入/输出相关的类统称为”流类“。C++中常用的几个流类及其关系如图所示。

▶ 为了避免多重继承的二义性,从ios派生istream和ostream时,均使用了virtual关键字(虚继承)
▶ istream类提供了流的大部分输入操作,对系统预定义的所有输入流重载提取运算符">>"。ostream类对系统预定义的所有输出流重载插入运算符”<<“
▶ 常用I/O流类列表

▶ C++的iostream类库常见的头文件有3个
(1) iostream: 包含操作所有输入/输出流所需的基本信息,因此大多数C++程序都应包含这个头文件。该文件含有4个标准流对象,提供了无格式化和格式化的I/O功能
(2) iomanip: 包含格式化I/O的带参数流操纵符,可用于指定数据输入/输出的格式。
(3) fstream: 包含处理文件的有关信息,提供建立文件、读/写文件的各种操作接口。
第二节 标准流对象
▶ 标准流对象(也称为标准流): 是为用户提供的常用外设与内存之间通信的通道,对数据进行解释和传输,提供必要的数据缓冲等
▶ C++在头文件iostream中定义了4个标准流对象
1) cin (标准输入流): 用于从键盘输入数据,是流类istream的对象
2) cout(标准输出流): 用于向屏幕输出数据,是流类ostream的对象, 可以重定向输出到文件
3) cerr(非缓冲错误输出流): cerr不使用缓冲区,直接向显示器输出信号,不能重定向。
4) clog (缓冲错误输出流): 先被存储到缓冲区中,缓冲区满或者刷新时才输出到屏幕。
▶ cout可以使用重定向函数freopen进行重定向输出到文件中保存
FILE * reopen(const char * path, const char * mode, FILE * stream);
功能: 是将stream按mode指定的模式重定向到路径path指向的文件。如果重定向时发生错误,则关闭原来的stream,函数返回NULL. mode可以是“w”(写)或“r”(读)方式。
例: 将标准输出cout重定向到文件
#include <iostream>
using namespace std;
int main() {
int x, y;
cin >> x >> y;
freopen("test.txt", "w", stdout); //将标准输出重定向到文件test.txt
if (y == 0) //除数为0则输出错误信息
{
cerr << "error>" << endl;
}
else
{
cout << x << "/" << y << "=" << x / y << endl;
}
fclose(stdout);
return 0;
}
注意: 'fopen': This function or variable may be unsafe. Consider using fopen_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
解决方案,项目 ->属性->c/c++ ->预处理器->点击预处理器定义,编辑,加入_CRT_SECURE_NO_WARNINGS,即可。 例: 读取文件中的前10个整数(假设文件中的整数超过10个), 然后计算这些整数的平均值。标准输入重定向文件
#include <iostream>
using namespace std;
int main()
{
int x, count, sum = 0;
freopen("input.dat", "r", stdin); //将标准输入重定向到文件input.dat
for (count = 0; count < 10; count++)
{//从输入流中读入10个整数进行处理
cin >> x;
cout <<"\t" << count << ": " << x;
sum += x;
}
cout << "前10个整数的平均值=" << 1.0 * sum / 10 << endl;
fclose(stdin);
return 0;
}
▶ C++在ios类中定义了错误状态字和有关函数

1) int eof() const;
功能: 测试是否到达文件尾,当文件操作结束遇到文件尾时,函数返回1;否则返回0。
在标准输入流cin中,可以通过按下<Ctrl+Z>组合键表示输入流的结束。
2) int fail() const;
功能: 判断流操作是否失败。失败返回1, 否则返回 0;
3) int good() const; int operator void * ()
功能:上述两个函数的功能相似,如果eofbit、failbit和badbit全部都没有被置位(即均为0), 则返回1(流正常), 否则返回 0;
4) int bad() const; int operator void!()
上述两个函数的功能相似,只要eofbit、failbit和badbit中有一位被置位(即为1),则返回1(流失败),否则返回0;
5) int rdstate() const;
功能: 返回流的当前状态
6) void clear (int nStata = 0)
功能:恢复或设置状态字,即将流状态恢复为正常
例:从文件中读入前10个整数,若文件中的整数不足10个,则全部读入,然后计算这些整数的平均值。
#include <iostream>
using namespace std;
int main()
{
int x, count, sum = 0;
freopen("input.dat", "r", stdin); //将标准输入重定向到文件input.dat
for (count = 0; count < 10; count++)
{
cin >> x;
if (cin.eof())
{
break;
}
else {
sum +=x;
}
}
cout << "前" << count << "个整数的平均值:=" << 1.0 * sum / count << endl;
fclose(stdin);
return 0;
}
例:将从键盘输入的数据进行累加
#include <iostream>
#include <string>
using namespace std;
int main()
{
char ch;
int sum = 0, count = 0, x;
cout << "请输入整数(按Ctrl + Z退出)" << endl;
do
{
while (cin >> x) //使用组合键<Ctrl+Z>结束输入
{
sum += x;
count++; //计算累加和
}
cout << "确实要退出输入请按Q";
cin.clear(); //为了保证下次的确认仍能起作用,状态字清0
cin >> ch;
} while (toupper(ch) != 'Q'); //将ch字符转换成大写字母
cout << "sum=" << sum << ", count=" << count << endl;
return 0;
}
第三节 控制I/O格式
一、流操纵符
▶ C++进行I/O格式控制的方式一般有使用流操纵符、设置标志字和调用成员函数。
▶ C++中常见数据类型的默认I/O格式

▶ C++在iostream中提供了一些常用的无参数的流操纵符(也称为格式控制符)

▶ dec * 的星号 “*” 不是操纵符的一部分,表示是默认设置。在默认情况下整数采用 十进行形式输出。
▶ 在头文件iomanip中定义了一些用于格式控制的流操纵符
后补
第四节 调用cout成员函数
▶ 在cout中调用成员函数控制输出格式,其作用和对应的流操作符相同

例: 使用cout中的函数控制输出格式
#include <iostream>
using namespace std;
int main()
{
double values[] = { 1.23, 20.3456, 300.4567, 4000.56789, 50000.1234567 };
cout.fill('*'); //设置填充字符为星号*
for (int i = 0; i < sizeof(values) / sizeof(double); i++)
{
cout << "values[" << i << "]=(";
cout.width(10); //设置输出宽度
cout << values[i] << ")" << endl;
}
cout.fill(' '); //设置填充字符为空格
for (int i = 0; i < sizeof(values) / sizeof(double); i++)
{
cout << "values[" << i << "]=(";
cout.width(10); //设置输出宽度
cout.precision(i + 3); //设置保留有效数字
cout << values[i] << ")" << endl;
}
return 0;
}
#include <iostream>
using namespace std;
int main()
{
char c = 'a', str[80] = "0123456789abcdefghijklmn";
int x = 65;
cout << "cout.put('a'):";
cout.put('a');
cout << "\ncout.put(c+25):";
cout.put(c + 25);
cout << "\ncout.put(x):";
cout.put(x);
cout << "\ncout.write(str, 20):";
cout.write(str, 20); //将str前20个字节写入到输出流
return 0;
}
第五节 调用cin成员函数
-
cin.intget();
功能:与C语言中getchar()基本相同。从输入流中读入一个字符(包括空白字符), 返回值就是该字符的ASCII码。
如果碰到输入结束符,则返回值为系统常量EOF(End Of File, 文件结束标记)
EOF在iostream类中定义为一整型常量,值为-1.
例:采用EOF判断输入是否结束
#include <iostream> using namespace std; int main() { int n = 0; char ch; while ((ch = cin.get()) != EOF) //当文件没有结束时继续进行循环 {//cin.get()功能类似C语言的getchar(), 从键盘读入1个字符 cout.put(ch); //类似C语言的putchar(), 输出1个字符 n++; } cout << "输入字符共计:" << n << endl; return 0; }注意:单独的一行按<Ctrl + Z>组合键后再按<Enter> 键就代表文件输入结束。cin.get()的返回值就是EOF,使while循环正常退出。
-
getline()函数
cin.getline(char *buf, int n);
功能:是从输入流中的当前字符开始读取 n-1 个字符到缓冲区buf,或读到'\n'为止,结尾自动添加串结束标记'\n'
cin.getline(char *buf, int n, char ch);
功能:是从输入流中的当前字符开始读取 n-1 个字符到缓冲区buf, 或读到字符ch为止,结尾自动添加'\0'.
例: getline()函数功能演示
#include <iostream> using namespace std; int main() { char buf[10]; int i = 0; //cin.getline(buf,10)函数的功能从输入流 // 中的当前字符开始读取9个字符到buf,或读到\n为止, // 结尾自动添加 串结束标记\n //若输入流的一行超过9个字符,则会出错,循环结束 cout << "开始输入" << endl; while (cin.getline(buf, 10)) { cout << ++i << ":" << buf << endl; cout << "last:" << buf << endl; return 0; } } -
cin.eof()
功能:用于判断输入流是否已经结束。返回值为true(1) 表示输入结束。
-
cin.ignore(int n, char ch)
功能: 是跳过输入流中的n个字符,或跳过ch及其之前的所有字符。两个参数都有默认值,因此cin.ignore()等效于cin.ignore(1, EOF), 即跳过一个字符。该函数常用于跳过输入中的无用部分,以便提取有用的部分。
例:从输入的字符串中提取电话号码
#include <iostream> using namespace std; int main() { char str[30]; while (!cin.eof()) //当输入流没有结束时继续循环 { cin.ignore(10, ':'); //在cin流中跳过" : "之前的全部字符 if (!cin.eof()) { cin >> str; cout << str << endl; } } return 0; }5.cin.peek();-
功能:返回输入流中的当前字符,但是并不将该字符吕输入流中取走-----相当于只是"看了一眼"将要读入的下一个字符,因此叫"窥视"。
-
▶ cin.peek()不会跳过输入流中的空格和回车符
▶ 在输入流已经结束的情况下,cin.peek()返回EOF.
第八章 文件操作
第一节 文件基本概念和文件流类
一、文件的概念
▶ C++根据文件数据的编码方式不同分为文本文件和二进制文件
文本文件:文件中的每个字节都是一个ASCII码。
二进制文件:C++中将非文本文件统称为二进制文件。
▶ 根据存取方式不同分为顺序存取文件和随机访问文件
顺序存取文件:就是按照文件中数据存储次序进行顺序操作
随机访问文件:是根据应用的需要,通过命令移动位置指针直接定位到文件内需要的位置并进行数据操作。
▶ 对文件的基本操作分为读文件和写文件 读文件:是将文件中的数据读入内存之中,也称为"输入"
写文件:就是将内存中的数据存入文件之中,也称为”输出“。
二、C++文件流
▶ C++标准类库中有3个流类可以用于文件操作,这3个类统称为文件流类,分别如下
1)ifstream: 用于从文件中读取数据。
2) ofstream: 用于向文件中写入数据。
3)fstream: 即可用于从文件中读取数据,又可用于向文件中写入数据。
使用这3个流类时,程序中需要包含fstream头文件
第二节 打开和关闭文件
一、打开文件
▶ 打开文件有以下两个目的:
1) 建立关联. 通过指定文件名,建立起文件和文件流对象的关联。
2) 指明文件的使用方式和文件格式。使用方式有只读、只写、既读又写、在文件末尾追加数据4种,文件格是文件方式或二进制方式。
▶ 打开文件的方式有以下两种。
1)先建立流对象,然后调用 open() 函数连接外部文件。格式如下:
流类名 对象名; 对象名.open(文件名, 模式);
2) 调用流类带参数的构造函数,在建立流对象的同时连接外部文件。格式如下:
流类名 对象名 open(文件名, 模式);
文件打开模式标记

例: 要从当前文件夹中名为 data.txt 的文件中读取数据,语句打开文件的语句:
ifstream inFile; //建立输入文件流对象
inFile.open("data.txt", ios::in); //只读模式打开
或: ifstream inFile; inFile.open("data.txt"); //默认的是只读模式打开
使用第二种方式打开,语句如下:
ifstream inFile("data.txt", ios::in);
例: C盘的C2018文件夹中打开(创建)一个名为newfile的二进制文件,用于保存程序产生的数据,可以使用如下语句打开文件:
ostream outFile; //建立输入文件流对象
outFile.open("c:\2018\newFile", ios::out | ios::binary) ; //连接文件,写方式打开二进制文件
第二种方式打开文件
ofstream outFile("c:\c2018\newfile", ios::out | ios::binary) ;
二、关闭文件
当一个文件操作完毕应及时关闭文件。其作用:
① 系统会将缓存区中的数据完整地写入文件
② 添加 文件结束标记
③ 切断流对象与外部文件的连接。
例如: 关闭打开的文件
ifstream inFile;
inFile.open("file.txt, ios::in); //打开文件file.txt
inFile.close(); //关闭文件
第三节 文件读写操作
一、读写文本文件
例: 对文本文件score.txt进行输入/输出
#include <iostream>
#include <fstream>
using namespace std;
int main() {
char id[11], name[21];
int score;
ofstream outFile;
outFile.open("score.txt", ios::out); //以写方式打开文本
if (!outFile) //条件成立,则说明文件打开出错
{
cout << "创建文件失败" << endl;
return 0;
}
cout << "请输入: 学号 姓名 成绩(以Ctr+z结束输入)\n";
while (cin >> id >> name >> score) {
outFile << id << " " << name << " " << score << endl; //向流中插入数据
}
outFile.close(); //关闭文件
return 0;
}
例: 读入学生成绩文件score.txt并显示内容
#include <iostream>
#include <fstream>
#include <iomanip> //manipulator "操纵器"的缩写
using namespace std;
int main()
{
char id[11], name[21];
int score;
ifstream inFile;
inFile.open("score.txt", ios::in); //以读方式打开文本文件
if (!inFile) //条件成立,则说明文件打开出错
{
cout << "打开文件失败" << endl;
return 0;
}
cout << "学生学号 姓名\t\t\t成绩\n";
while (inFile >> id >> name >> score) //读入文件,读到文件尾结束循环
{
cout << left << setw(10) << id << " " << setw(20) << name << " " << setw(3) << right << score << endl;
}
inFile.close(); //关闭文件
return 0;
}
例: 读入文本文件,加上行号后显示内容
第四节 随机访问文件
(后续补)
更多推荐
所有评论(0)