个人对于C++语法的系统性学习,并不完全涵盖所有语法,仅包含常用语法。
class Score{
public:
void setScore(int m, int f);
void showScore();
private:
int mid_exam;
int fin_exam;
};
对一个具体的类来讲,类声明格式中的3个部分public, protected, private并非一定要全有,但至少要有其中的一个部分。一般情况下,一个类的数据成员应该声明为私有成员,成员函数声明为共有成员。这样,内部的数据整个隐蔽在类中,在类的外部根本就无法看到,使数据得到有效的保护,也不会对该类以外的其余部分造成影响,程序之间的相互作用就被降低到最小。
类声明中的关键字private、protected、public可以以任意顺序出现。
若私有部分处于类的第一部分时,关键字private可以省略。这样,如果一个类体中没有一个访问权限关键字,则其中的数据成员和成员函数都默认为私有的。
不能在类声明中给数据成员赋初值。
将成员函数直接定义在类的内部,相当于隐式声明了内联成员函数。
在类中,使用inline定义内联函数时,必须将类的声明和内联成员函数的定义都放在同一个文件(或同一个头文件)中,否则编译时无法进行代码置换。
class Score{
public:
void setScore(int m, int f);
void showScore();
private:
int mid_exam;
int fin_exam;
};
void Score::setScore(int m, int f)
{
mid_exam = m;
fin_exam = f;
}
void Score::showScore()
{
cout << "期中成绩: " << mid_exam << endl;
cout << "期末成绩:" << fin_exam << endl;
}
Score op, *sc;
sc = &op;
sc->setScore(99, 100);
op.showScore();
私有成员只能被类中的成员函数访问,不能在类的外部,通过类的对象进行访问。
一般来说,公有成员是类的对外接口,而私有成员是类的内部数据和内部实现,不希望外界访问。将类的成员划分为不同的访问级别有两个好处:一是信息隐蔽,即实现封装,将类的内部数据与内部实现和外部接口分开,这样使该类的外部程序不需要了解类的详细实现;二是数据保护,即将类的重要信息保护起来,以免其他程序进行不恰当的修改。
构造函数是一种特殊的成员函数,它主要用于为对象分配空间,进行初始化。
构造函数的名字必须与类名相同,而不能由用户任意命名。
它可以有任意类型的参数,但不能具有返回值。
它不需要用户来调用,而是在建立对象时自动执行。
构造函数一般声明为共有成员,但它不需要也不能像其他成员函数那样被显式地调用。
类成员是按照它们在类里被声明的顺序进行初始化的,与它们在成员初始化列表中列出的顺序无关。
带默认参数的构造函数
#include
using namespace std;
class Score{
public:
Score(int m = 0, int f = 0); //带默认参数的构造函数
void setScore(int m, int f);
void showScore();
private:
int mid_exam;
int fin_exam;
};
Score::Score(int m, int f) : mid_exam(m), fin_exam(f)
{
cout << "构造函数使用中..." << endl;
}
void Score::setScore(int m, int f)
{
mid_exam = m;
fin_exam = f;
}
void Score::showScore()
{
cout << "期中成绩: " << mid_exam << endl;
cout << "期末成绩:" << fin_exam << endl;
}
int main()
{
Score op1(99, 100);
Score op2(88);
Score op3;
op1.showScore();
op2.showScore();
op3.showScore();
return 0;
}
注意上述的成员初始化列表
析构函数
析构函数也是一种特殊的成员函数。它执行与构造函数相反的操作,通常用于撤销对象时的一些清理任务,如释放分配给对象的内存空间等。析构函数有以下一些特点:
在以下情况中,当对象的生命周期结束时,析构函数会被自动调用:
如果定义了一个全局对象,则在程序流程离开其作用域时,调用该全局对象的析构函数。
如果一个对象定义在一个函数体内,则当这个函数被调用结束时,该对象应该被释放,析构函数被自动调用。
若一个对象是使用new运算符创建的,在使用delete运算符释放它时,delete会自动调用析构函数。
如下示例:
#include
#include
using namespace std;
class Student{
private:
char *name;
char *stu_no;
float score;
public:
Student(char *name1, char *stu_no1, float score1);
~Student();
void modify(float score1);
void show();
};
Student::Student(char *name1, char *stu_no1, float score1)
{
name = new char[strlen(name1) + 1];
strcpy(name, name1);
stu_no = new char[strlen(stu_no1) + 1];
strcpy(stu_no, stu_no1);
score = score1;
}
Student::~Student()
{
delete []name;
delete []stu_no;
}
void Student::modify(float score1)
{
score = score1;
}
void Student::show()
{
cout << "姓名: " << name << endl;
cout << "学号: " << stu_no << endl;
cout << "成绩:" << score << endl;
}
int main()
{
Student stu("雪女", "2020199012", 99);
stu.modify(100);
stu.show();
return 0;
}
对没有定义构造函数的类,其公有数据成员可以用初始值列表进行初始化。
class A{
public:
char name[10];
int no;
};
A a = {"chen", 23};
在一个类中,构造函数可以重载,注意需要使用不同的参数列表。
拷贝构造函数
拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。拷贝构造函数的作用是在建立一个新对象时,使用一个已存在的对象去初始化这个新对象。
拷贝构造函数具有以下特点:
类名::类名(const 类名 &对象名)
{
拷贝构造函数的函数体;
}
class Score{
public:
Score(int m, int f); //构造函数
Score();
Score(const Score &p); //拷贝构造函数
~Score(); //析构函数
void setScore(int m, int f);
void showScore();
private:
int mid_exam;
int fin_exam;
};
Score::Score(int m, int f)
{
mid_exam = m;
fin_exam = f;
}
Score::Score(const Score &p)
{
mid_exam = p.mid_exam;
fin_exam = p.fin_exam;
}
调用拷贝构造函数的一般形式为:
类名 对象2(对象1);
类名 对象2 = 对象1;
Score sc1(98, 87);
Score sc2(sc1); //调用拷贝构造函数
Score sc3 = sc2; //调用拷贝构造函数
调用拷贝构造函数的三种情况:
浅拷贝和深拷贝
浅拷贝,就是由默认的拷贝构造函数所实现的数据成员逐一赋值。通常默认的拷贝构造函数是能够胜任此工作的,但若类中含有指针类型的数据,则这种按数据成员逐一赋值的方法会产生错误。
解决方法就是重定义拷贝构造函数,为其变量重新生成内存空间。
Student::Student(const Student& stu)
{
name = new char[strlen(stu.name) + 1];
if (name != 0) {
strcpy(name, stu.name);
score = stu.score;
}
}
this指针保存当前对象的地址,称为自引用指针。
void Sample::copy(Sample& xy)
{
if (this == &xy) return;
*this = xy;
}
用对象指针访问对象数组
Score score[2];
score[0].setScore(90, 99);
score[1].setScore(67, 89);
Score *p;
p = score; //将对象score的地址赋值给p
p->showScore();
p++; //对象指针变量加1
p->showSccore();
Score *q;
q =&score[1]; //将第二个数组元素的地址赋值给对象指针变量q
C++支持两种类型的字符串,第一种是C语言中介绍过的、包括一个结束符’\0’(即以NULL结束)的字符数组,标准库函数提供了一组对其进行操作的函数,可以完成许多常用的字符串操作。
C++标准库中声明了一种更方便的字符串类型,即字符串类string,类string提供了对字符串进行处理所需要的操作。使用string类必须在程序的开始包括头文件string,即要有以下语句:#include string
常用的string类运算符如下:
=、+、+=、==、!=、<、<=、>、>=、[](访问下标对应字符)、>>(输入)、<<(输出)
#include
#include
using namespace std;
int main()
{
string str1 = "ABC";
string str2("dfdf");
string str3 = str1 + str2;
cout<< "str1 = " << str1 << " str2 = " << str2 << " str3 = " << str3 << endl;
str2 += str2;
str3 += "aff";
cout << "str2 = " << str2 << " str3 = " << str3 << endl;
cout << "str1[1] = " << str1[1] << " str1 == str2 ? " << (str1 == str2) << endl;
string str = "ABC";
cout << "str == str1 ? " << (str == str1) << endl;
return 0;
}
#include
using namespace std;
class Point{
public:
int x;
int y;
Point(int x1, int y1) : x(x1), y(y1) //成员初始化列表
{ }
int getDistance()
{
return x * x + y * y;
}
};
void changePoint1(Point point) //使用对象作为函数参数
{
point.x += 1;
point.y -= 1;
}
void changePoint2(Point *point) //使用对象指针作为函数参数
{
point->x += 1;
point->y -= 1;
}
void changePoint3(Point &point) //使用对象引用作为函数参数
{
point.x += 1;
point.y -= 1;
}
int main()
{
Point point[3] = {Point(1, 1), Point(2, 2), Point(3, 3)};
Point *p = point;
changePoint1(*p);
cout << "the distance is " << p[0].getDistance() << endl;
p++;
changePoint2(p);
cout << "the distance is " << p->getDistance() << endl;
changePoint3(point[2]);
cout << "the distance is " << point[2].getDistance() << endl;
return 0;
}
静态数据成员
在一个类中,若将一个数据成员说明为static,则这种成员被称为静态数据成员。与一般的数据成员不同,无论建立多少个类的对象,都只有一个静态数据成员的拷贝。从而实现了同一个类的不同对象之间的数据共享。
定义静态数据成员的格式如下:static 数据类型 数据成员名;
对象名.静态数据成员名;
对象指针->静态数据成员名;
静态成员函数
在类定义中,前面有static说明的成员函数称为静态成员函数。静态成员函数属于整个类,是该类所有对象共享的成员函数,而不属于类中的某个对象。静态成员函数的作用不是为了对象之间的沟通,而是为了处理静态数据成员。
与静态数据成员类似,调用公有静态成员函数的一般格式有如下几种:
类名::静态成员函数名(实参表);
对象.静态成员函数名(实参表);
对象指针->静态成员函数名(实参表);
下面对静态成员函数的使用再做几点说明:
#include
using namespace std;
class Score{
private:
int mid_exam;
int fin_exam;
static int count; //静态数据成员,用于统计学生人数
static float sum; //静态数据成员,用于统计期末累加成绩
static float ave; //静态数据成员,用于统计期末平均成绩
public:
Score(int m, int f);
~Score();
static void show_count_sum_ave(); //静态成员函数
};
Score::Score(int m, int f)
{
mid_exam = m;
fin_exam = f;
++count;
sum += fin_exam;
ave = sum / count;
}
Score::~Score()
{
}
/*** 静态成员初始化 ***/
int Score::count = 0;
float Score::sum = 0.0;
float Score::ave = 0.0;
void Score::show_count_sum_ave()
{
cout << "学生人数: " << count << endl;
cout << "期末累加成绩: " << sum << endl;
cout << "期末平均成绩: " << ave << endl;
}
int main()
{
Score sco[3] = {Score(90, 89), Score(78, 99), Score(89, 88)};
sco[2].show_count_sum_ave();
Score::show_count_sum_ave();
return 0;
}
类的主要特点之一是数据隐藏和封装,即类的私有成员(或保护成员)只能在类定义的范围内使用,也就是说私有成员只能通过它的成员函数来访问。但是,有时为了访问类的私有成员而需要在程序中多次调用成员函数,这样会因为频繁调用带来较大的时间和空间开销,从而降低程序的运行效率。为此,C++提供了友元来对私有或保护成员进行访问。友元包括友元函数和友元类。
友元函数
友元函数既可以是不属于任何类的非成员函数,也可以是另一个类的成员函数。友元函数不是当前类的成员函数,但它可以访问该类的所有成员,包括私有成员、保护成员和公有成员。
在类中声明友元函数时,需要在其函数名前加上关键字friend。此声明可以放在公有部分,也可以放在保护部分和私有部分。友元函数可以定义在类内部,也可以定义在类外部。
#include
using namespace std;
class Score{
private:
int mid_exam;
int fin_exam;
public:
Score(int m, int f);
void showScore();
friend int getScore(Score &ob);
};
Score::Score(int m, int f)
{
mid_exam = m;
fin_exam = f;
}
int getScore(Score &ob)
{
return (int)(0.3 * ob.mid_exam + 0.7 * ob.fin_exam);
}
int main()
{
Score score(98, 78);
cout << "成绩为: " << getScore(score) << endl;
return 0;
}
说明:
一个函数同时定义为两个类的友元函数
#include
#include
using namespace std;
class Score; //对Score类的提前引用说明
class Student{
private:
string name;
int number;
public:
Student(string na, int nu) {
name = na;
number = nu;
}
friend void show(Score &sc, Student &st);
};
class Score{
private:
int mid_exam;
int fin_exam;
public:
Score(int m, int f) {
mid_exam = m;
fin_exam = f;
}
friend void show(Score &sc, Student &st);
};
void show(Score &sc, Student &st) {
cout << "姓名:" << st.name << " 学号:" << st.number << endl;
cout << "期中成绩:" << sc.mid_exam << " 期末成绩:" << sc.fin_exam << endl;
}
int main() {
Score sc(89, 99);
Student st("白", 12467);
show(sc, st);
return 0;
}
将成员函数声明为友元函数
一个类的成员函数可以作为另一个类的友元,它是友元函数中的一种,称为友元成员函数。友元成员函数不仅可以访问自己所在类对象中的私有成员和公有成员,还可以访问friend声明语句所在类对象中的所有成员,这样能使两个类相互合作、协调工作,完成某一任务。
#include
#include
using namespace std;
class Score; //对Score类的提前引用说明
class Student{
private:
string name;
int number;
public:
Student(string na, int nu) {
name = na;
number = nu;
}
void show(Score &sc);
};
class Score{
private:
int mid_exam;
int fin_exam;
public:
Score(int m, int f) {
mid_exam = m;
fin_exam = f;
}
friend void Student::show(Score &sc);
};
void Student::show(Score &sc) {
cout << "姓名:" << name << " 学号:" << number << endl;
cout << "期中成绩:" << sc.mid_exam << " 期末成绩:" << sc.fin_exam << endl;
}
int main() {
Score sc(89, 99);
Student st("白", 12467);
st.show(sc);
return 0;
}
说明:
一个类的成员函数作为另一个类的友元函数时,必须先定义这个类。并且在声明友元函数时,需要加上成员函数所在类的类名;
友元类
可以将一个类声明为另一个类的友元
当一个类被说明为另一个类的友元类时,它所有的成员函数都成为另一个类的友元函数,这就意味着作为友元类中的所有成员函数都可以访问另一个类中的所有成员。
class Y{
···
};
class X{
friend Y; //声明类Y为类X的友元类
};
友元关系不具有交换性和传递性。
在一个类中内嵌另一个类的对象作为数据成员,称为类的组合。该内嵌对象称为对象成员,又称为子对象。
class Y{
···
};
class X{
Y y;
···
};
常类型的引入就是为了既保护数据共享又防止数据被改动。常类型是指使用类型修饰符const说明的类型,常类型的变量或对象成员的值在程序运行期间是不可改变的。
常引用
如果在说明引用时用const修饰,则被说明的引用为常引用。常引用所引用的对象不能被更新。如果用常引用做形参,便不会产生对实参的不希望的更改。
int a = 5;
const int& b = a;
此时再对b赋值是非法的。
---------------------------
int add(const int& m, const int& n) {
return m + n;
}
在此函数中对变量m和变量n更新时非法的
常对象
如果在说明对象时用const修饰,则被说明的对象为常对象。常对象中的数据成员为常量且必须要有初值。
const Date date(2021, 5, 31);
常对象成员
1、常数据成员
如果在一个类中说明了常数据成员,那么构造函数就只能通过成员初始化列表对该数据成员进行初始化,而任何其他函数都不能对该成员赋值。
2、常成员函数
class Date{
private:
int year;
int month;
int day;
public:
Date(int y, int m, int d) : year(y), month(m), day(d){
}
void showDate();
void showDate() const;
};
void Date::showDate() {
//···
}
void Date::showDate() const {
//···
}
说明:
继承可以在已有类的基础上创建新的类,新类可以从一个或多个已有类中继承成员函数和数据成员,而且可以重新定义或加进新的数据和函数,从而形成类的层次或等级。其中,已有类称为基类或父类,在它基础上建立的新类称为派生类或子类。
案例:
#include
#include
using namespace std;
class Person{
private:
string name;
string id_number;
int age;
public:
Person(string name1, string id_number1, int age1) {
name = name1;
id_number = id_number1;
age = age1;
}
~Person() {
}
void show() {
cout << "姓名: " << name << " 身份证号: " << id_number << " 年龄: " << age << endl;
}
};
class Student:public Person{
private:
int credit;
public:
Student(string name1, string id_number1, int age1, int credit1):Person(name1, id_number1, credit1) {
credit = credit1;
}
~Student() {
}
void show() {
Person::show();
cout << "学分: " << credit << endl;
}
};
int main() {
Student stu("白", "110103**********23", 12, 123);
stu.show();
return 0;
}
派生类对基类成员的访问规则
基类的成员可以有public、protected、private 3种访问属性,基类的成员函数可以访问基类中其他成员,但是在类外通过基类的对象,就只能访问该基类的公有成员。同样,派生类的成员也可以有public、protected、private 3种访问属性,派生类的成员函数可以访问派生类中自己增加的成员,但是在派生类外通过派生类的对象,就只能访问该派生类的公有成员。
需要注意:如果基类中包含private类型成员,那么不论何种继承方式,派生类中无法直接访问该成员。
派生类对基类成员的访问形式主要有以下两种:
内部访问:由派生类中新增的成员函数对基类继承来的成员的访问。
对象访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问。
构造函数的主要作用是对数据进行初始化。在派生类中,如果对派生类新增的成员进行初始化,就需要加入派生类的构造函数。与此同时,对所有从基类继承下来的成员的初始化工作,还是由基类的构造函数完成,但是基类的构造函数和析构函数不能被继承,因此必须在派生类的构造函数中对基类的构造函数所需要的参数进行设置。同样,对撤销派生类对象的扫尾、清理工作也需要加入新的析构函数来完成。
构造函数的调用严格地按照先调用基类的构造函数,后调用派生类的构造函数的顺序执行。析构函数的调用顺序与构造函数的调用顺序正好相反,先调用派生类的析构函数,后调用基类的析构函数。
派生类斜体样式构造函数和析构函数的构造规则
派生类构造函数的一般格式为:
派生类名(参数总表):基类名(参数表) {
派生类新增数据成员的初始化语句
}
-----------------------------------------------------------------
含有子对象的派生类的构造函数:
派生类名(参数总表):基类名(参数表0),子对象名1(参数表1),...,子对象名n(参数表n)
{
派生类新增成员的初始化语句
}
说明:
派生类可以声明与基类成员同名的成员。在没有虚函数的情况下,如果在派生类中定义了与基类成员同名的成员,则称派生类成员覆盖了基类的同名成员,在派生类中使用这个名字意味着访问在派生类中声明的成员。为了在派生类中使用与基类同名的成员,必须在该成员名之前加上基类名和作用域标识符“::”,即
基类名::成员名
访问声明的方法就是把基类的保护成员或共有成员直接写在私有派生类定义式中的同名段中,同时给成员名前冠以基类名和作用域标识符“::”。利用这种方法,该成员就成为派生类的保护成员或共有成员了。
class B:private A{
private:
int y;
public:
B(int x1, int y1) : A(x1) {
y = y1;
}
A::show; //访问声明
};
访问声明在使用时应注意以下几点:
声明多继承派生类的一般形式如下:
class 派生类名:继承方式1 基类名1,...,继承方式n 基类名n {
派生类新增的数据成员和成员函数
};
默认的继承方式是private
多继承派生类的构造函数与析构函数:
与单继承派生类构造函数相同,多重继承派生类构造函数必须同时负责该派生类所有基类构造函数的调用。
多继承构造函数的调用顺序与单继承构造函数的调用顺序相同,也是遵循先调用基类的构造函数,再调用对象成员的构造函数,最后调用派生类构造函数的原则。析构函数的调用与之相反。
虚基类的作用:如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最低层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。在访问这些同名成员时,必须在派生类对象名后增加直接基类名,使其唯一地标识一个成员,以免产生二义性。
#include
#include
using namespace std;
class Base{
protected:
int a;
public:
Base(){
a = 5;
cout << "Base a = " << a << endl;
}
};
class Base1: public Base{
public:
Base1() {
a = a + 10;
cout << "Base1 a = " << a << endl;
}
};
class Base2: public Base{
public:
Base2() {
a = a + 20;
cout << "Base2 a = " << a << endl;
}
};
class Derived: public Base1, public Base2{
public:
Derived() {
cout << "Base1::a = " << Base1::a << endl;
cout << "Base2::a = " << Base2::a << endl;
}
};
int main() {
Derived obj;
return 0;
}
代码执行结果如下:
Base a = 5
Base1 a = 15
Base a = 5
Base2 a = 25
Base1::a = 15
Base2::a = 25
虚基类的声明:
如果在上列中类base只存在一个拷贝(即只有一个数据成员a),那么对a的访问就不会产生二义性。在C++中,可以通过将这个公共的基类声明为虚基类来解决这个问题。这就要求从类base派生新类时,使用关键字virtual将base声明为虚基类。
上述代码修改如下:
class Base1:virtual public Base{
public:
Base1() {
a = a + 10;
cout << "Base1 a = " << a << endl;
}
};
class Base2:virtual public Base{
public:
Base2() {
a = a + 20;
cout << "Base2 a = " << a << endl;
}
};
运行结果如下:
Base a = 5
Base1 a = 15
Base2 a = 35
Base1::a = 35
Base2::a = 35
虚基类的初始化:
虚基类的初始化与一般的多继承的初始化在语法上是一样的,但构造函数的调用顺序不同。在使用虚基类机制时应该注意以下几点:
在一定条件下,不同类型的数据之间可以进行类型转换,如可以将整型数据赋值给双精度型变量。在赋值之前,先把整型数据转换成双精度数据,然后再把它赋给双精度变量。这种不同数据类型之间的自动转换和赋值,称为赋值兼容。在基类和派生类对象之间也存有赋值兼容关系,基类和派生类对象之间的赋值兼容规则是指在需要基类对象的任何地方,都可以用子类的对象代替。
例如,下面声明的两个类:
class Base{
·····
};
class Derived: public Base{
·····
};
根据赋值兼容规则,在基类Base的对象可以使用的任何地方,都可以使用派生类Derived的对象来代替,但只能使用从基类继承来的成员。具体的表现在以下几个方面:
Base b;
Derived d;
b = d;
Derived d;
Base &br = d;
Derived d;
Base *bp = &d;
所谓多态性就是不同对象收到相同的消息时,产生不同的动作。这样,就可以用同样的接口访问不同功能的函数,从而实现“一个接口,多种方法”。
从实现的角度来讲,多态可以划分为两类:编译时的多态和运行时的多态。在C++中,多态的实现和连编这一概念有关。所谓连编就是把函数名与函数体的程序代码连接在一起的过程。静态连编就是在编译阶段完成的连编。编译时的多态是通过静态连编来实现的。静态连编时,系统用实参与形参进行匹配,对于同名的重载函数便根据参数上的差异进行区分,然后进行连编,从而实现了多态性。运行时的多态是用动态连编实现的。动态连编时运行阶段完成的,即当程序调用到某一函数名时,才去寻找和连接其程序代码,对面向对象程序设计而言,就是当对象接收到某一消息时,才去寻找和连接相应的方法。
一般而言,编译型语言(如C,Pascal)采用静态连编,而解释型语言(如LISP)采用动态连编。静态连编要求在程序编译时就知道调用函数的全部信息。因此,这种连编类型的函数调用速度快、效率高,但缺乏灵活性;而动态连编方式恰好相反,采用这种连编方式,一直要到程序运行时才能确定调用哪个函数,它降低了程序的运行效率,但增强了程序的灵活性。纯粹的面向对象程序语言由于其执行机制是消息传递,所以只能采用动态连编。C++实际上采用了静态连编和动态连编相结合的方式。
在C++中,编译时多态性主要是通过函数重载和运算符重载实现的;运行时多态性主要是通过虚函数来实现的。
虚函数的定义是在基类中进行的,它是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual,从而提供一种接口界面。定义虚函数的方法如下:
virtual 返回类型 函数名(形参表) {
函数体
}
在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。
#include
#include
using namespace std;
class Family{
private:
string flower;
public:
Family(string name = "鲜花"): flower(name) { }
string getName() {
return flower;
}
virtual void like() {
cout << "家人喜欢不同的花: " << endl;
}
};
class Mother: public Family{
public:
Mother(string name = "月季"): Family(name) { }
void like() {
cout << "妈妈喜欢" << getName() << endl;
}
};
class Daughter: public Family{
public:
Daughter(string name = "百合"): Family(name) { }
void like() {
cout << "女儿喜欢" << getName() << endl;
}
};
int main() {
Family *p;
Family f;
Mother mom;
Daughter dau;
p = &f;
p->like();
p = &mom;
p->like();
p = &dau;
p->like();
return 0;
}
程序运行结果如下:
家人喜欢不同的花:
妈妈喜欢月季
女儿喜欢百合
C++规定,如果在派生类中,没有用virtual显式地给出虚函数声明,这时系统就会遵循以下的规则来判断一个成员函数是不是虚函数:该函数与基类的虚函数是否有相同的名称、参数个数以及对应的参数类型、返回类型或者满足赋值兼容的指针、引用型的返回类型。
下面对虚函数的定义做几点说明:
在一个派生类中重新定义基类的虚函数是函数重载的另一种形式。
多继承可以视为多个单继承的组合,因此,多继承情况下的虚函数调用与单继承下的虚函数调用由相似之处。
如果在主函数中用new运算符建立一个派生类的无名对象和定义一个基类的对象指针,并将无名对象的地址赋值给这个对象指针,当用delete运算符撤销无名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。
Base *p;
p = new Derived;
delete p;
-----------------
输出:调用基类Base的析构函数
原因是当撤销指针p所指的派生类的无名对象,而调用析构函数时,采用了静态连编方式,只调用了基类Base的析构函数。
如果希望程序执行动态连编方式,在用delete运算符撤销派生类的无名对象时,先调用派生类的析构函数,再调用基类的析构函数,可以将基类的析构函数声明为虚析构函数。一般格式为
virtual ~类名(){
·····
}
虽然派生类的析构函数与基类的析构函数名字不相同,但是如果将基类的析构函数定义为虚函数,由该类所派生的所有派生类的析构函数也都自动成为虚函数。示例如下
#include
#include
using namespace std;
class Base{
public:
virtual ~Base() {
cout << "调用基类Base的析构函数..." << endl;
}
};
class Derived: public Base{
public:
~Derived() {
cout << "调用派生类Derived的析构函数..." << endl;
}
};
int main() {
Base *p;
p = new Derived;
delete p;
return 0;
}
纯虚函数是在声明虚函数时被“初始化为0的函数”,声明纯虚函数的一般形式如下:
virtual 函数类型 函数名(参数表) = 0;
声明为纯虚函数后,基类中就不再给出程序的实现部分。纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要重新定义。
如果一个类至少有一个纯虚函数,那么就称该类为抽象类,对于抽象类的使用有以下几点规定:
应用C++的多态性,计算三角形、矩形和圆的面积。
#include
using namespace std;
/*** 定义一个公共基类 ***/
class Figure{
protected:
double x, y;
public:
Figure(double a, double b): x(a), y(b) { }
virtual void getArea() //虚函数
{
cout << "No area computation defind for this class.\n";
}
};
class Triangle: public Figure{
public:
Triangle(double a, double b): Figure(a, b){ }
//虚函数重定义,用于求三角形的面积
void getArea(){
cout << "Triangle with height " << x << " and base " << y;
cout << " has an area of " << x * y * 0.5 << endl;
}
};
class Square: public Figure{
public:
Square(double a, double b): Figure(a, b){ }
//虚函数重定义,用于求矩形的面积
void getArea(){
cout << "Square with dimension " << x << " and " << y;
cout << " has an area of " << x * y << endl;
}
};
class Circle: public Figure{
public:
Circle(double a): Figure(a, a){ }
//虚函数重定义,用于求圆的面积
void getArea(){
cout << "Circle with radius " << x ;
cout << " has an area of " << x * x * 3.14 << endl;
}
};
int main(){
Figure *p;
Triangle t(10.0, 6.0);
Square s(10.0, 6.0);
Circle c(10.0);
p = &t;
p->getArea();
p = &s;
p->getArea();
p = &c;
p->getArea();
return 0;
}
运算符重载是面向对象程序设计的重要特征。
运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据导致不同的行为。
下面的案例实现+号运算符重载:
#include
using namespace std;
class Complex{
private:
double real, imag;
public:
Complex(double r = 0.0, double i = 0.0): real(r), imag(i) { }
friend Complex operator+(Complex& a, Complex& b) {
Complex temp;
temp.real = a.real + b.real;
temp.imag = a.imag + b.imag;
return temp;
}
void display() {
cout << real;
if (imag > 0) cout << "+";
if (imag != 0) cout << imag << "i" << endl;
}
};
int main()
{
Complex a(2.3, 4.6), b(3.6, 2.8), c;
a.display();
b.display();
c = a + b;
c.display();
c = operator+(a, b);
c.display();
return 0;
}
利用模板机制可以显著减少冗余信息,能大幅度地节约程序代码,进一步提高面向对象程序的可重用性和可维护性。模板是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数,从而实现代码的重用,使得一段程序可以用于处理多种不同类型的对象,大幅度地提高程序设计的效率。
所谓函数模板,实际上是建立一个通用函数,其函数返回类型和形参类型不具体指定,用一个虚拟的类型来代表,这个通用函数就称为函数模板。在调用函数时,系统会根据实参的类型(模板实参)来取代模板中的虚拟类型,从而实现不同函数的功能。
函数的声明格式如下
template <typename 类型参数>
返回类型 函数名(模板形参表)
{
函数体
}
也可以定义为如下形式
template <class 类型参数>
返回类型 函数名(模板形参表)
{
函数体
}
实际上,template是一个声明模板的关键字,它表示声明一个模板。类型参数(通常用C++标识符表示,如T、type等)实际上是一个虚拟的类型名,使用前并未指定它是哪一种具体的类型,但使用函数模板时,必须将类型实例化。类型参数前需加关键字typename或class,typename和class的作用相同,都是表示一个虚拟的类型名(即类型参数)。
例1:一个与指针有关的函数模板
#include
using namespace std;
template <typename T>
T Max(T *array, int size = 0) {
T max = array[0];
for (int i = 1 ; i < size; i++) {
if (array[i] > max) max = array[i];
}
return max;
}
int main() {
int array_int[] = {783, 78, 234, 34, 90, 1};
double array_double[] = {99.02, 21.9, 23.90, 12.89, 1.09, 34.9};
int imax = Max(array_int, 6);
double dmax = Max(array_double, 6);
cout << "整型数组的最大值是:" << imax << endl;
cout << "双精度型数组的最大值是:" << dmax << endl;
return 0;
}
例2:函数模板的重载
#include
using namespace std;
template <class Type>
Type Max(Type x, Type y) {
return x > y ? x : y;
}
template <class Type>
Type Max(Type x, Type y, Type z) {
Type t = x > y ? x : y;
t = t > z ? t : z;
return t;
}
int main() {
cout << "33,66中最大值为 " << Max(33, 66) << endl;
cout << "33,66,44中最大值为 " << Max(33, 66, 44) << endl;
return 0;
}
注意:
所谓类模板,实际上就是建立一个通用类,其数据成员、成员函数的返回类型和形参类型不具体指定,用一个虚拟的类型来代表。使用类模板定义对象时,系统会根据实参的类型来取代类模板中虚拟类型,从而实现不同类的功能。
template <typename T>
class Three{
private:
T x, y, z;
public:
Three(T a, T b, T c) {
x = a; y = b; z = c;
}
T sum() {
return x + y + z;
}
}
上面的例子中,成员函数(其中含有类型参数)是定义在类体内的。但是,类模板中的成员函数也可以在类模板体外定义。此时,若成员函数中有类型参数存在,则C++有一些特殊的规定:
例如,上例中成员函数sum()在类外定义时,应该写成
template<typename T>
T Three<T>::sum() {
return x + y + z;
}
**例子:**栈类模板的使用
#include
#include
using namespace std;
const int size = 10;
template <class T>
class Stack{
private:
T stack[size];
int top;
public:
void init() {
top = 0;
}
void push(T t);
T pop();
};
template <class T>
void Stack<T>::push(T t) {
if (top == size) {
cout << "Stack is full!" << endl;
return;
}
stack[top++] = t;
}
template <class T>
T Stack<T>::pop() {
if (top == 0) {
cout << "Stack is empty!" <<endl;
return 0;
}
return stack[--top];
}
int main() {
Stack<string> st;
st.init();
st.push("aaa");
st.push("bbb");
cout << st.pop() << endl;
cout << st.pop() << endl;
return 0;
}
程序中常见的错位分为两大类:编译时错误和运行时错误。编译时的错误主要是语法错误,如关键字拼写错误、语句末尾缺分号、括号不匹配等。运行时出现的错误统称为异常,对异常的处理称为异常处理。
C++处理异常的办法:如果在执行一个函数的过程中出现异常,可以不在本函数中立即处理,而是发出一个信息,传给它的上一级(即调用函数)来解决,如果上一级函数也不能处理,就再传给其上一级,由其上一级处理。如此逐级上传,如果到最高一级还无法处理,运行系统一般会自动调用系统函数terminate(),由它调用abort终止程序。
**例子:**输入三角形的三条边长,求三角形的面积。当输入边的长度小于0时,或者当三条边都大于0时但不能构成三角形时,分别抛出异常,结束程序运行。
#include
#include
using namespace std;
double triangle(double a, double b, double c) {
double s = (a + b + c) / 2;
if (a + b <= c || a + c <= b || b + c <= a) {
throw 1.0; //语句throw抛出double异常
}
return sqrt(s * (s - a) * (s - b) * (s - c));
}
int main() {
double a, b, c;
try {
cout << "请输入三角形的三个边长(a, b, c): " << endl;
cin >> a >> b >> c;
if (a < 0 || b < 0 || c < 0) {
throw 1; //语句throw抛出int异常
}
while (a > 0 && b > 0 && c > 0) {
cout << "a = " << a << " b = " << b << " c = " << c << endl;
cout << "三角形的面积 = " << triangle(a, b, c) << endl;
cin >> a >> b >> c;
if (a <= 0 || b <= 0 || c <= 0) {
throw 1;
}
}
} catch (double) {
cout << "这三条边不能构成三角形..." << endl;
} catch (int) {
cout << "边长小于或等于0..." << endl;
}
return 0;
}