C++基础语法04之面向对象篇
C++ 在 C 语言的基础上增加了面向对象编程,C++ 支持面向对象程序设计。类是 C++ 的核心特性,通常被称为用户定义的类型。
类用于指定对象的形式,它包含了数据表示法和用于处理数据的方法。
类中的数据和方法称为类的成员。函数在一个类内时也被称为类的成员。
定义一个类,本质上是定义一个数据类型的蓝图。这实际上并没有定义任何数据,但它定义了类的名称意味着什么,
也就是说,它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作。
类定义是以关键字 class 开头,后跟类的名称。类的主体是包含在一对花括号中。
类定义后必须跟着一个分号或一个声明列表。
例如,我们使用关键字 class 定义 Box 数据类型,如下所示:
class Box
{
public:
double length; // Length of a box
double breadth; // Breadth of a box
double height; // Height of a box
};
关键字 public 确定了类成员的访问属性。在类对象作用域内,公共成员在类的外部是可访问的。
您也可以指定类的成员为 private 或 protected,这个我们稍后会进行讲解。
类提供了对象的蓝图,所以基本上,对象是根据类来创建的。
声明类的对象,就像声明基本类型的变量一样。下面的语句声明了类 Box 的两个对象:
Box Box1; // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box
对象 Box1 和 Box2 都有它们各自的数据成员。
类的对象的公共数据成员可以使用直接成员访问运算符 (.) 来访问。为了更好地理解这些概念,让我们尝试一下下面的实例:
#include
using namespace std;
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
int main( )
{
Box Box1; // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box
double volume = 0.0; // 用于存储体积
// box 1 详述
Box1.height = 5.0;
Box1.length = 6.0;
Box1.breadth = 7.0;
// box 2 详述
Box2.height = 10.0;
Box2.length = 12.0;
Box2.breadth = 13.0;
// box 1 的体积
volume = Box1.height * Box1.length * Box1.breadth;
cout << "Box1 的体积:" << volume <<endl;
// box 2 的体积
volume = Box2.height * Box2.length * Box2.breadth;
cout << "Box2 的体积:" << volume <<endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Box1 的体积:210
Box2 的体积:1560
#include
using namespace std;
class Cube
{
public:
double length;
double width;
double height;
}; // 注意必须以分号结尾
int main()
{
// 声明两个对象
Cube cube_1;
// 0x7fff5f732b38
cout << &cube_1 << endl;
double result = 0;
cube_1.height = 5;
cube_1.width = 6;
cube_1.length = 1.2;
// 打印 体积
result = cube_1.height * cube_1.width * cube_1.length;
cout << result << endl;
}
需要注意的是,private私有的成员和protected受保护的成员不能使用直接成员访问运算符 (.) 来直接访问。
我们将在后续的教程中学习如何访问私有成员和受保护的成员。
到目前为止,我们已经对 C++ 的类和对象有了基本的了解。下面的列表中还列出了其他一些 C++ 类和对象相关的概念,可以点击相应的链接进行学习。
概念 | 描述 |
---|---|
类成员函数 | 类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。 |
类访问修饰符 | 类成员可以被定义为 public、private 或 protected。默认情况下是定义为 private。 |
构造函数 & 析构函数 | 类的构造函数是一种特殊的函数,在创建一个新的对象时调用。类的析构函数(destructor)也是一种特殊的函数,在删除所创建的对象时调用。 |
C++ 拷贝构造函数 | 拷贝构造函数,是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。 |
C++ 友元函数 | 友元函数可以访问类的 private 和 protected 成员。 |
C++ 内联函数 | 通过内联函数,编译器试图在调用函数的地方扩展函数体中的代码。 |
C++ 中的 this 指针 | 每个对象都有一个特殊的指针 this,它指向对象本身。 |
C++ 中指向类的指针 | 指向类的指针方式如同指向结构的指针。实际上,类可以看成是一个带有函数的结构。 |
C++ 类的静态成员 | 类的数据成员和函数成员都可以被声明为静态的。 |
类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。
类成员函数是类的一个成员,它可以操作类的任意对象,可以访问对象中的所有成员。
让我们看看之前定义的类 Box,现在我们要使用成员函数来访问类的成员,而不是直接访问这些类的成员:
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度
double getVolume(void);// 返回体积
};
成员函数可以定义在类定义内部,或者单独使用范围解析运算符 :: 来定义。
在类定义中 定义的成员函数 把函数声明为内联的,即便没有使用 inline 标识符。???Excuse Me???
所以您可以按照如下方式定义 Volume() 函数:
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度
double getVolume(void)
{
return length * breadth * height;
}
};
您也可以在类的外部使用范围解析运算符 :: 定义该函数,如下所示:
double Box::getVolume(void)
{
return length * breadth * height;
}
在这里,需要强调一点,在范围解析运算符::之前必须使用类名。
调用成员函数是在对象上 使用点运算符(.),这样它就能操作与该对象相关的数据,如下所示:
Box myBox; // 创建一个对象
myBox.getVolume(); // 调用该对象的成员函数
让我们使用上面提到的概念来设置和获取类中不同的成员的值:
#include
using namespace std;
class Cube
{
public:
double _length;
double _width;
double _height;
// 仅仅声明成员函数:
void setLength(double length);
void setWidth(double width);
void setHeight(double height);
double giveMeResult(void);
}; // 注意必须以分号结尾
// 在类外部,使用范围解析符 定义成员函数
// 在类的外部写函数时,千万不要忘记 Cube::
void Cube::setLength(double length)
{
_length = length;
}
// 在类的外部写函数时,千万不要忘记 Cube::
void Cube::setWidth(double width)
{
_width = width;
}
// 在类的外部写函数时,千万不要忘记 Cube::
void Cube::setHeight(double height)
{
_height = height;
}
// 在类的外部写函数时,千万不要忘记 Cube::
double Cube::giveMeResult(void)
{
return _length * _width * _height;
}
int main()
{
// 声明两个对象
Cube cube_1;
// 0x7fff5f732b38
cout << &cube_1 << endl;
double result = 0;
cube_1.setHeight(5);
cube_1.setWidth(6);
cube_1.setLength(1.2);
// 打印 体积
result = cube_1.giveMeResult();
cout << result << endl;
}
当上面的代码被编译和执行时,它会产生下列结果:
数据隐藏是面向对象编程的一个重要特点,它防止外部调用者的函数直接访问类类型的内部成员。
类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。
关键字 public、private、protected称为访问说明符。
一个类可以有多个 public、protected 或 private 标记区域。
每个标记区域在下一个标记区域开始之前或者在遇到类主体结束右括号之前都是有效的。
成员和类的默认访问修饰符是 private。
class Base {
public:
// public sumurai come here
protected:
// protected girls come here
private:
// private babies come here
};
公有成员在程序中类的外部是可访问的。您可以不使用任何成员函数来设置和获取公有变量的值,如下所示:
#include
using namespace std;
class Girl
{
public:
int _age;
void setAge(int age);
int getAge(void);
};
// 必须使用范围i解析Girl::
void Girl::setAge(int age)
{
_age = age;
}
int Girl::getAge(void)
{
return _age;
}
int main ()
{
Girl tiger;
// 1.使用set和get
tiger.setAge(16);
cout << tiger.getAge() << endl;
// 2.不使用set和get
tiger._age = 15;
cout << tiger.getAge() << endl;
}
#include
using namespace std;
class Line
{
public:
double length;
void setLength( double len );
double getLength( void );
};
// 成员函数定义
double Line::getLength(void)
{
return length ;
}
void Line::setLength( double len )
{
length = len;
}
// 程序的主函数
int main( )
{
Line line;
// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <
当上面的代码被编译和执行时,它会产生下列结果:
Length of line : 6
Length of line : 10
私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。
只有类和友元函数可以访问私有成员。
默认情况下,类的所有成员都是私有的。
例如在下面的类中,width 是一个私有成员,这意味着,如果您没有使用任何访问修饰符,类的成员将被假定为私有成员:
class Box
{
double width;
public:
double length;
void setWidth( double wid );
double getWidth( void );
};
实际操作中,我们一般会在私有区域定义数据,在公有区域定义相关的函数,以便在类的外部也可以调用这些函数,如下所示:
#include
using namespace std;
class Girl
{
public:
void setAge(int age);
int getAge(void);
private:
int _age;
};
// 必须使用范围解析Girl::
void Girl::setAge(int age)
{
_age = age;
}
int Girl::getAge(void)
{
return _age;
}
int main ()
{
Girl tiger;
// 1.使用set和get
tiger.setAge(16);
cout << tiger.getAge() << endl;
}
#include
using namespace std;
class Box
{
public:
double length;
void setWidth( double wid );
double getWidth( void );
private:
double width;
};
// 成员函数定义
double Box::getWidth(void)
{
return width ;
}
void Box::setWidth( double wid )
{
width = wid;
}
// 程序的主函数
int main( )
{
Box box;
// 不使用成员函数设置长度
box.length = 10.0; // OK: 因为 length 是公有的
cout << "Length of box : " << box.length <
当上面的代码被编译和执行时,它会产生下列结果:
Length of box : 10
Width of box : 10
保护成员变量或函数与私有成员十分相似,但有一点不同,保护成员在派生类(即子类)中是可访问的。
在下一个章节中,您将学习到派生类和继承的知识。
现在您可以看到下面的实例中,我们从父类 Box 派生了一个子类 smallBox。
下面的实例与前面的实例类似,在这里 width 成员可被派生类 smallBox 的任何成员函数访问。
#include
using namespace std;
class Box
{
protected:
double width;
};
class SmallBox:Box // SmallBox 是派生类
{
public:
void setSmallWidth( double wid );
double getSmallWidth( void );
};
// 子类的成员函数
double SmallBox::getSmallWidth(void)
{
return width ;
}
void SmallBox::setSmallWidth( double wid )
{
width = wid;
}
// 程序的主函数
int main( )
{
SmallBox box;
// 使用成员函数设置宽度
box.setSmallWidth(5.0);
cout << "Width of box : "<< box.getSmallWidth() << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Width of box : 5
#include
using namespace std;
class Person
{
protected:
// 子类可以访问
int _age;
};
// : 表示继承
class Girl:Person
{
public:
void setAge(int age);
int getAge(void);
};
// 必须使用范围解析Girl::
void Girl::setAge(int age)
{
_age = age;
}
int Girl::getAge(void)
{
return _age;
}
int main ()
{
Girl tiger;
// 1.使用set和get
tiger.setAge(16);
cout << tiger.getAge() << endl;
}
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。
构造函数可用于为某些成员变量设置初始值。
下面的实例有助于更好地理解构造函数的概念:
#include
using namespace std;
class Girl
{
public:
void setAge(int age);
int getAge(void);
// 同名的无参构造函数,函数名必须是类名,且必须没有返回值
Girl();
private:
int _age;
};
// 必须使用范围解析Girl::
// 同名的无参构造函数,函数名必须是类名,且必须没有返回值
Girl::Girl()
{
cout << "alloc init " << endl;
}
void Girl::setAge(int age)
{
_age = age;
}
int Girl::getAge(void)
{
return _age;
}
int main ()
{
Girl tiger;
// 1.使用set和get
tiger.setAge(16);
cout << tiger.getAge() << endl;
}
#include
using namespace std;
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line(void)
{
cout << "Object is being created" << endl;
}
void Line::setLength( double len )
{
length = len;
}
double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line;
// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <
当上面的代码被编译和执行时,它会产生下列结果:
Object is being created
Length of line : 6
默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。
这样在创建对象时就会给对象赋初始值,如下面的例子所示:
#include
using namespace std;
class Girl
{
public:
void setAge(int age);
int getAge(void);
// 带参数的构造函数,函数名必须是类名,且不能有返回值
Girl(int age);
private:
int _age;
};
// 必须使用范围解析Girl::
// 同名的带参构造函数,函数名必须是类名,且必须没有返回值
Girl::Girl(int age)
{
cout << "alloc init " << endl;
_age = age;
}
void Girl::setAge(int age)
{
_age = age;
}
int Girl::getAge(void)
{
return _age;
}
int main ()
{
Girl tiger(15);
cout << tiger.getAge() << endl;
// 1.使用set和get
tiger.setAge(16);
cout << tiger.getAge() << endl;
}
#include
using namespace std;
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(double len); // 这是构造函数
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line( double len)
{
cout << "Object is being created, length = " << len << endl;
length = len;
}
void Line::setLength( double len )
{
length = len;
}
double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line(10.0);
// 获取默认设置的长度
cout << "Length of line : " << line.getLength() <
当上面的代码被编译和执行时,它会产生下列结果:
Object is being created, length = 10
Length of line : 10
Length of line : 6
使用初始化列表来初始化字段:
Line::Line( double len): length(len)
{
cout << "Object is being created, length = " << len << endl;
}
上面的语法等同于如下语法:
Line::Line( double len)
{
cout << "Object is being created, length = " << len << endl;
length = len;
}
假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,同理地,您可以使用上面的语法,只需要在不同的字段使用逗号进行分隔,如下所示:
C::C( double a, double b, double c): X(a), Y(b), Z(c)
{
....
}
#include
using namespace std;
class Girl
{
public:
void setAge(int age);
int getAge(void);
// 带参数的构造函数,函数名必须是类名,且不能有返回值
Girl(int age);
private:
int _age;
};
// 必须使用范围解析Girl::
// 同名的带参构造函数,函数名必须是类名,且必须没有返回值
// 使用初始化列表 来初始化字段
// 核心代码
Girl::Girl(int age):_age(age)
{
cout << "alloc init " << endl;
}
void Girl::setAge(int age){
_age = age;
}
int Girl::getAge(void){
return _age;
}
int main ()
{
Girl tiger(15);
cout << tiger.getAge() << endl;
// 1.使用set和get
tiger.setAge(16);
cout << tiger.getAge() << endl;
}
#include
using namespace std;
class Cube
{
public:
double giveMeResult(void);
// 带参数的构造函数,函数名必须是类名,且不能有返回值
Cube(int length,int widht,int height);
private:
int _length;
int _widht;
int _height;
};
// 必须使用范围解析Girl::
// 同名的带参构造函数,函数名必须是类名,且必须没有返回值
// 使用初始化列表 来初始化字段
// 核心代码
Cube::Cube(int length,int widht,int height):_length(length),_widht(widht),_height(height)
{
cout << "alloc init " << endl;
}
double Cube::giveMeResult()
{
return _length * _widht * _height;
}
int main ()
{
Cube Cube(5,6,7);
cout << Cube.giveMeResult() << endl;
}
#include
using namespace std;
struct Girl
{
int age ;
// 无参构造函数
Girl()
{
cout << "1_Construct Girl" << endl ;
}
// 拷贝构造函数
Girl(const Girl& littleGirl)
{
cout << "2_Copy constructor for Girl" << endl ;
this->age = littleGirl.age ;
}
// 重载赋值运算符 =
Girl& operator = (const Girl& cuteGirl)
{
cout << "3_assignment for Girl" << endl ;
this->age = cuteGirl.age ;
return *this;
}
}; // 类的定义必须用;结尾
// 一部动漫拥有一个女主角
struct Anime
{
Girl _actress ;
// 带参数的构造函数
Anime(Girl &actress)
{
_actress = actress ;
}
};
int main ()
{
// 有个女孩叫面码
Girl menma ;
// 有部动漫名叫:那朵花;里面的女主叫面码
Anime anohana(menma) ;
}
/*
运行结果:
1_Construct Girl (来自于这行代码"Girl menma"中menma对象的实例化时的构造函数)
1_Construct Girl (来自于这行代码Anime anohana(menma)中anohana对象的实例化时的构造函数)
3_assignment for Girl (来自于这行代码_actress = actress 运算符的重载)
*/
#include
using namespace std;
struct Girl
{
int age ;
// 无参构造函数
Girl()
{
cout << "1_Construct Girl" << endl ;
}
// 拷贝构造函数
Girl(const Girl& littleGirl)
{
cout << "2_Copy constructor for Girl" << endl ;
this->age = littleGirl.age ;
}
// 重载赋值运算符 =
Girl& operator = (const Girl& cuteGirl)
{
cout << "3_assignment for Girl" << endl ;
this->age = cuteGirl.age ;
return *this;
}
}; // 类的定义必须用;结尾
// 一部动漫拥有一个女主角
struct Anime
{
Girl _actress ;
// 当带参数的构造函数时的运行结果:要3次构造化
/*
运行结果:
1_Construct Girl (来自于这行代码"Girl menma"中menma对象的实例化时的构造函数)
1_Construct Girl (来自于这行代码Anime anohana(menma)中anohana对象的实例化时的构造函数)
3_assignment for Girl (来自于这行代码_actress = actress 运算符的重载)
*/
// Anime(Girl &actress)
// {
// _actress = actress ;
// }
// 当使用 参数列表初始化 时,只运行了2次构造函数
/*
运行结果:
1_Construct Girl 这一行结果来自"Girl menma"
2_Copy constructor for Girl 这一行来自"Anime anohana(menma)中的初始化参数列表时的拷贝构造函数"
*/
Anime(Girl &actress):_actress(actress){
}
};
int main ()
{
// 有个女孩叫面码
Girl menma ;
// 有部动漫名叫:那朵花;里面的女主叫面码
Anime anohana(menma) ;
}
#include
using namespace std;
struct Girl
{
// 初始化列表
Girl(int age):_age(age){}
int _age ;
};
struct Anime
{
Girl _girl;
// 第1种构造函数
// Anime(Girl &cuteGirl)
// {
// _girl = cuteGirl ;
// }
// 第2种构造函数
Anime(Girl &girl):_girl(girl){
}
};
int main ()
{
// 有个女孩叫面码
Girl menma(16);
// 有部动漫名叫:那朵花;里面的女主叫面码
Anime anohana(menma);
/*
如果Anime的构造函数是第1种的话,会报错:
因为Girl是没有无参数的构造函数的
error: constructor for 'Anime' must
explicitly initialize the member '_girl' which does not have a default
constructor
Anime(Girl &cuteGirl)
^
如果Anime的构造函数是第2种,即初始化列表的话,就可以跳过Girl的无参数构造函数,进行对girl的拷贝复制
*/
}
#include
using namespace std;
struct foo
{
int _i ;
int _j ;
foo(int x):_j(x), _i(_j){
}
// 报警告:_i值未定义
// 因为定义在前面的是_i,所以_i必须比_j先初始化,但是_i又是根据_j来初始化的,所以导致_i未初始化,故程序出现异常
/*
warning: field 'j' is uninitialized
when used here [-Wuninitialized]
foo(int x):j(x), i(j){}
^
*/
};
int main ()
{
}
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。
析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
下面的实例有助于更好地理解析构函数的概念:
#include
using namespace std;
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数声明
~Line(); // 这是析构函数声明
private:
double length;
};
// 成员函数定义,包括构造函数
Line::Line(void)
{
cout << "Object is being created" << endl;
}
Line::~Line(void)
{
cout << "Object is being deleted" << endl;
}
void Line::setLength( double len )
{
length = len;
}
double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line;
// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <
当上面的代码被编译和执行时,它会产生下列结果:
Object is being created
Length of line : 6
Object is being deleted
#include
using namespace std;
class Line
{
private:
double _length;
public:
Line();
~Line();
void setLength(double length);
double giveMeLength(void);
};
// 构造函数 没有返回值,连void都不能有
Line::Line(void)
{
cout << "构造函数" << endl;
}
Line::~Line(void)
{
cout << "析构函数" << endl;
}
void Line::setLength(double length)
{
_length = length;
}
double Line::giveMeLength(void)
{
return _length;
}
int main ()
{
Line singleLine;
singleLine.setLength(6.7);
cout << singleLine.giveMeLength() << endl;
}
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。
拷贝构造函数通常用于:
通过使用另一个同类型的对象来初始化新创建的对象。
复制对象,并把它作为参数传递给函数。
复制对象,并从函数返回这个对象。
如果在类中没有定义拷贝构造函数,编译器会自行定义一个拷贝构造函数。
???Excuse Me???
如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。
拷贝构造函数的最常见形式如下:
classname (const classname &obj) {
// 构造函数的主体
}
在这里,obj 是一个对象引用,该对象是用于初始化另一个对象的。
#include
using namespace std;
class Line
{
public:
int getLength( void );
Line( int len ); // 简单的构造函数
Line( const Line &obj); // 拷贝构造函数
~Line(); // 析构函数
private:
int *ptr;
};
// 成员函数定义,包括构造函数
Line::Line(int len)
{
cout << "Normal constructor allocating ptr" << endl;
// 为指针分配内存
ptr = new int;
*ptr = len;
}
Line::Line(const Line &obj)
{
cout << "Copy constructor allocating ptr." << endl;
ptr = new int;
*ptr = *obj.ptr; // copy the value
}
Line::~Line(void)
{
cout << "Freeing memory!" << endl;
delete ptr;
}
int Line::getLength( void )
{
return *ptr;
}
void display(Line obj)
{
cout << "Length of line : " << obj.getLength() <
当上面的代码被编译和执行时,它会产生下列结果:
Normal constructor allocating ptr
Copy constructor allocating ptr.
Length of line : 10
Freeing memory!
Freeing memory!
下面的实例对上面的实例稍作修改,通过使用已有的同类型的对象来初始化新创建的对象:
#include
using namespace std;
class Line
{
public:
int getLength( void );
Line( int len ); // 简单的构造函数
Line( const Line &obj); // 拷贝构造函数
~Line(); // 析构函数
private:
int *ptr;
};
// 成员函数定义,包括构造函数
Line::Line(int len)
{
cout << "Normal constructor allocating ptr" << endl;
// 为指针分配内存
ptr = new int;
*ptr = len;
}
Line::Line(const Line &obj)
{
cout << "Copy constructor allocating ptr." << endl;
ptr = new int;
*ptr = *obj.ptr; // copy the value
}
Line::~Line(void)
{
cout << "Freeing memory!" << endl;
delete ptr;
}
int Line::getLength( void )
{
return *ptr;
}
void display(Line obj)
{
cout << "Length of line : " << obj.getLength() <
当上面的代码被编译和执行时,它会产生下列结果:
Normal constructor allocating ptr
Copy constructor allocating ptr.
Copy constructor allocating ptr.
Length of line : 10
Freeing memory!
Copy constructor allocating ptr.
Length of line : 10
Freeing memory!
Freeing memory!
Freeing memory!
代码如下:
#include
using namespace std;
class Line
{
private:
// 用指针 指向int型(保存长度)
int *_point_of_length;
public:
// 先写析构函数
~Line();
// 带参数的构造函数
Line(int length);
// 拷贝构造函数
Line(const Line &oneLine);
// 获取长度
int giveMeLength(void);
};
// 先写析构函数
Line::~Line(void)
{
cout << "析构函数" << endl;
// 删除动态创建的内存空间
delete _point_of_length;
}
// 带参数的构造函数
Line::Line(int length)
{
cout << "带参数的构造函数" << endl;
// 必须先为指针 动态分配内存???
// 等价于 int *p; p=(int *)malloc(sizeof(int));
_point_of_length = new int;
// 将长度用指针保存起来
*_point_of_length = length;
}
// 拷贝构造函数
Line::Line(const Line &oneLine)
{
cout << "拷贝构造函数" << endl;
// 必须先为指针 动态分配内存???
// 等价于 int *p; p=(int *)malloc(sizeof(int));
_point_of_length = new int;
// 将传进来的oneLine的长度length,用指针保存起来(copy value)
*_point_of_length = *(oneLine._point_of_length);
}
int Line::giveMeLength(void)
{
return *(_point_of_length);
}
int main ()
{
Line singleLine(520);
cout << singleLine.giveMeLength() << endl << endl;
// 下面的拷贝构造函数会使用已有的对象singleLine来初始化新的对象shadowLine
Line shadowLine = singleLine;
cout << shadowLine.giveMeLength() << endl << endl;
}
运行效果如下:
类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。
尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
友元可以是一个函数,该函数被称为友元函数;
友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。
如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend,如下所示:
class Box
{
double width;
public:
double length;
friend void printWidth( Box box );
void setWidth( double wid );
};
声明类 ClassTwo 的所有成员函数作为类 ClassOne 的友元,需要在类 ClassOne 的定义中放置如下声明:
friend class ClassTwo;
请看下面的程序:
#include
using namespace std;
class Box
{
double width;
public:
friend void printWidth( Box box );
void setWidth( double wid );
};
// 成员函数定义
void Box::setWidth( double wid )
{
width = wid;
}
// 请注意:printWidth() 不是任何类的成员函数
void printWidth( Box box )
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "Width of box : " << box.width <
当上面的代码被编译和执行时,它会产生下列结果:
Width of box : 10
代码如下:
#include
using namespace std;
class Girl
{
private:
// 私有 罩杯
char _cup;
public:
// 隆胸手术
void makeCup(char cup);
// 使用友元进行 打印输出,参数必须是一个对象!
friend void showGirlCup(Girl girl);
};
// 赋值
void Girl::makeCup(char cup)
{
_cup = cup;
}
// 友元函数 进行输出private成员,参数必须是一个对象!
void showGirlCup(Girl girl)
{
// 使用成员访问运行符 .
cout << "girl's cup is:" << girl._cup << endl;
}
int main ()
{
Girl cuteGirl;
cuteGirl.makeCup('E');
// 友元函数 进行输出private成员,参数必须是一个对象!
showGirlCup(cuteGirl);
}
运行效果:
C++ 内联函数是通常与类一起使用。
如果一个函数是内联的,那么在编译时,编译器会把该函数体里的代码副本(一般来说是频繁被调用但又简单的代码)预先放置在每个调用该函数的地方。这样预编译的好处就是对于那种代码简单但频繁调用的函数,可以避免频繁开辟栈空间!
对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。
如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。
如果已定义的函数多于一行,编译器会忽略 inline 限定符。 ???Excuse Me???
在类内部的定义的函数(注意:必须是定义,而不仅仅是声明!!!)都是内联函数,即使没有使用 inline 说明符。
下面是一个实例,使用内联函数来返回两个数中的最大值:
#include
using namespace std;
inline int Max(int x, int y)
{
return (x > y)? x : y;
}
// 程序的主函数
int main( )
{
cout << "Max (20,10): " << Max(20,10) << endl;
cout << "Max (0,200): " << Max(0,200) << endl;
cout << "Max (100,1010): " << Max(100,1010) << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Max (20,10): 20
Max (0,200): 200
Max (100,1010): 1010
示例代码:
#include
using namespace std;
// 声明同时定义内联函数
inline int giveMeMaxNumber(int x,int y)
{
return x > y ? x : y;
}
int main ()
{
for (int i = 0; i < 100; ++i)
{
for (int j = 0; j < 100; ++j)
{
/*
使用了内联函数后,编译时,在这个频繁调用的地方,函数体内的代码会取代函数名
即:giveMeMaxNumber(i,j) 会被替换成i > j ? i : j
*/
cout << "max of ("<< i <<","<< j <<") is:" << giveMeMaxNumber(i,j) << endl;
}
}
}
运行效果:
1. 引入inline关键字的原因
在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。
栈空间就是指放置程序的局部数据(也就是函数内数据)的内存空间。
在系统下,栈空间是有限的,假如频繁大量的使用就会造成因栈空间不足而导致程序出错的问题,如,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭。
下面我们来看一个例子
#include
#include
using namespace std;
// 定义内联函数
inline string evenOrOdd(int num)
{
return (num % 2 == 0) ? "偶" : "奇";
}
int main ()
{
for (int i = 0; i < 10; ++i)
{
cout << i <<" is:" << evenOrOdd(i) << endl;
}
}
上面的例子就是标准的内联函数的用法,使用inline修饰带来的好处我们表面看不出来,其实,在内部的工作就是在每个for循环的内部任何调用evenOrOdd(i)的地方都换成了(i%2 == 0)?”偶”:”奇”,这样就避免了频繁调用函数对栈内存重复开辟所带来的消耗。
2. inline使用限制
inline的使用是有所限制的,inline只适合函数体内代码简单的函数使用,不能包含复杂的结构控制语句例如while、switch,并且不能内联函数本身不能是直接递归函数(即自己内部还调用自己的函数)。
3. inline仅是一个对编译器的建议
inline函数仅仅是一个对编译器的建议,所以最后能否真正内联,看编译器的意思,它如果认为函数不复杂,能在调用点展开,就会真正内联,并不是说声明了内联就会内联,声明内联只是一个建议而已。
4. 建议:inline函数的定义放在头文件中
其次,因为内联函数要在调用点展开,所以编译器必须随处可见内联函数的定义,要不然就成了非内联函数的调用了。
所以,这要求每个调用了内联函数的文件都出现了该内联函数的定义。
因此,将内联函数的定义放在头文件里实现是合适的,省却你为每个文件实现一次的麻烦。
声明跟定义要一致:如果在每个文件里都实现一次该内联函数的话,那么,最好保证每个定义都是一样的,否则,将会引起未定义的行为。如果不是每个文件里的定义都一样,那么,编译器展开的是哪一个,那要看具体的编译器而定。所以,最好将内联函数定义放在头文件中。
5. 类中的成员函数与inline
定义在类中的成员函数缺省都是内联的,如果在类定义时就在类内给出函数定义,那当然最好。
如果在类中只给了声明,却未给出成员函数定义,然而又想内联该函数的话,那么在类外部定义的时候要加上inline,否则就认为不是内联的。
例如,
class A
{
public:void Foo(int x, int y) { // do something } // 默认会自动地认为是内联函数 }
将成员函数的定义体放在类声明之中虽然能带来书写上的方便,但不是一种良好的编程风格,上例应该改成:
// 头文件里,一般只放函数的声明
class A
{
public:
void Foo(int x, int y); }
// 在类外部 使用inline关键字 定义内联函数
inline void A::Foo(int x, int y){}
6. inline 是一种“用于实现的关键字”
关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。
如下风格的函数Foo 不能成为内联函数:
inline void Foo(int x, int y); // inline 仅与函数声明放在一起,无意义 void Foo(int x, int y){}
而如下风格的函数Foo 则成为内联函数:
void Foo(int x, int y);
inline void Foo(int x, int y) {} // inline 与函数定义体放在一起,成为了内联函数
所以说,inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。
一般地,用户可以阅读函数的声明,但是看不到函数的定义。
尽管在大多数教科书中内联函数的声明、定义体前面都加了inline 关键字,但我认为inline不应该出现在函数的声明中。
这个细节虽然不会影响函数的功能,但是体现了高质量C++/C 程序设计风格的一个基本原则:
声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。
7. 慎用inline
内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。
另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
类的构造函数和析构函数容易让人误解成使用内联更有效。
要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。
所以不要随便地将 构造函数和析构函数 的定义体放在类声明中。
一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了 inline不应该出现在函数的声明中)。
8.总结
内联函数并不是一个增强性能的灵丹妙药。只有当函数非常短小的时候它才能得到我们想要的效果;
但是,如果函数并不是很短而且在很多地方都被调用的话,那么将会使得可执行体的体积增大。
最令人烦恼的还是当编译器拒绝内联的时候。在实现中,结果很不尽人意。
虽然有一些编译器能够足够的聪明来指出哪些函数可以内联哪些不能,但是大多数编译器就不那么聪明了,
因此这就需要我们的经验来判断。如果内联函数不能增强性能,就避免使用它!
在 C++ 中,每一个对象都能通过 this 指针来访问 自己的地址。this 指针是所有成员函数的隐含参数。
因此,在成员函数内部,它可以用来指向调用对象。
友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。
下面的实例有助于更好地理解 this 指针的概念:
#include
using namespace std;
class Box
{
public:
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
}
double Volume()
{
return length * breadth * height;
}
int compare(Box box)
{
return this->Volume() > box.Volume();
}
private:
double length; // Length of a box
double breadth; // Breadth of a box
double height; // Height of a box
};
int main(void)
{
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box Box2(8.5, 6.0, 2.0); // Declare box2
if(Box1.compare(Box2))
{
cout << "Box2 is smaller than Box1" <
当上面的代码被编译和执行时,它会产生下列结果:
Constructor called.
Constructor called.
Box2 is equal to or larger than Box1
代码如下 :
#include
#include
using namespace std;
class Cube
{
private:
double _length;
double _width;
double _height;
public:
// 声明构造函数带参数
Cube(double length,double width,double height);
// 得到体积
double giveMeResult();
// 对象间比较体积大小
int compareResult(Cube oneCube);
};
// 定义构造函数
Cube::Cube(double length,double width,double height)
{
_length = length;
_width = width;
_height = height;
}
// 定义获取体积的函数
double Cube::giveMeResult()
{
return _length * _width * _height;
}
// 定义比较Cube大小的函数
int Cube::compareResult(Cube oneCube)
{
// 一个用指针-> 一个用点语法.
return this->giveMeResult() > oneCube.giveMeResult();
}
int main ()
{
Cube cube_1(5,6,7);
cout << cube_1.giveMeResult() << endl;
Cube cube_2(6.7,1.314,5.2);
cout << cube_2.giveMeResult() << endl;
cout << cube_2.compareResult(cube_1) << endl;
}
运行效果如下:
注:类与结构体唯一的区别就是:权限一个是默认private,一个是默认public
一个指向类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 ->,就像访问指向结构的指针一样。
与所有的指针一样,您必须在使用指针之前,对指针进行初始化。
下面的实例有助于更好地理解指向类的指针的概念:
#include
using namespace std;
class Box
{
public:
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
}
double Volume()
{
return length * breadth * height;
}
private:
double length; // Length of a box
double breadth; // Breadth of a box
double height; // Height of a box
};
int main(void)
{
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box Box2(8.5, 6.0, 2.0); // Declare box2
Box *ptrBox; // Declare pointer to a class.
// 保存第一个对象的地址
ptrBox = &Box1;
// 现在尝试使用成员访问运算符来访问成员
cout << "Volume of Box1: " << ptrBox->Volume() << endl;
// 保存第二个对象的地址
ptrBox = &Box2;
// 现在尝试使用成员访问运算符来访问成员
cout << "Volume of Box2: " << ptrBox->Volume() << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Constructor called.
Constructor called.
Volume of Box1: 5.94
Volume of Box2: 102
代码:
#include
#include
using namespace std;
class Cube
{
private:
double _length;
double _width;
double _height;
public:
// 声明构造函数带参数
Cube(double length,double width,double height);
// 得到体积
double giveMeResult();
};
// 定义构造函数
Cube::Cube(double length,double width,double height)
{
_length = length;
_width = width;
_height = height;
}
// 定义获取体积的函数
double Cube::giveMeResult()
{
return _length * _width * _height;
}
int main ()
{
// 定义一个指向Cube类的指针
Cube *point_of_cube;
Cube cube_1(5,6,7);
point_of_cube = &cube_1;
// 通过指针进行调用
cout << cube_1.giveMeResult() << endl;
cout << point_of_cube->giveMeResult() << endl;
Cube cube_2(6.7,1.314,5.2);
point_of_cube = &cube_2;
// 通过指针进行调用
cout << cube_2.giveMeResult() << endl;
cout << point_of_cube->giveMeResult() << endl;
}
运行效果:
我们可以使用 static 关键字来把类成员定义为静态的。
当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。
静态成员在类的所有对象中是共享的。
如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。
我们不能把静态成员放置在类的定义中(在类定义中,只能声明静态成员变量),但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化,
如下面的实例所示。
下面的实例有助于更好地理解静态数据成员的概念:
#include
using namespace std;
class Box
{
public:
static int objectCount;
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
// 每次创建对象时增加 1
objectCount++;
}
double Volume()
{
return length * breadth * height;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
// 初始化类 Box 的静态成员
int Box::objectCount = 0;
int main(void)
{
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2
// 输出对象的总数
cout << "Total objects: " << Box::objectCount << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Constructor called.
Constructor called.
Total objects: 2
#include
#include
using namespace std;
class Cube
{
private:
double _length;
double _width;
double _height;
public:
// 只能在类内部声明静态成员变量!
static int cubeTotalNumber;
// 声明构造函数带参数
Cube(double length,double width,double height);
// 得到体积
double giveMeResult();
};
// 只能在类的外部 使用::定义和初始化类的静态成员变量
int Cube::cubeTotalNumber = 0;
// 定义构造函数
Cube::Cube(double length,double width,double height)
{
_length = length;
_width = width;
_height = height;
// 构造时,对象数目++
cubeTotalNumber++;
}
// 定义获取体积的函数
double Cube::giveMeResult()
{
return _length * _width * _height;
}
int main ()
{
// 如果想在对象还没创建之前就打印一下静态数据成员cubeTotalNumber,则可以使用静态函数成员
Cube cube_1(5,6,7);
cout << cube_1.cubeTotalNumber << endl;
Cube cube_2(1.3,1.4,520);
cout << cube_2.cubeTotalNumber << endl;
}
如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。
静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。
静态成员函数只能访问静态数据成员,或者其他静态成员函数,但不能访问类的非静态数据成员 以及 类外部的其他函数。
静态成员函数有一个类范围,他们不能访问类的与对象相关的 this 指针。
您可以使用静态成员函数来判断类的某些对象是否已被创建。
下面的实例有助于更好地理解静态函数成员的概念:
#include
using namespace std;
class Box
{
public:
static int objectCount;
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
// 每次创建对象时增加 1
objectCount++;
}
double Volume()
{
return length * breadth * height;
}
static int getCount()
{
return objectCount;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
// 初始化类 Box 的静态成员
int Box::objectCount = 0;
int main(void)
{
// 在创建对象之前输出对象的总数
cout << "Inital Stage Count: " << Box::getCount() << endl;
Box Box1(3.3, 1.2, 1.5); // 声明 box1
Box Box2(8.5, 6.0, 2.0); // 声明 box2
// 在创建对象之后输出对象的总数
cout << "Final Stage Count: " << Box::getCount() << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Inital Stage Count: 0
Constructor called.
Constructor called.
Final Stage Count: 2
代码:
#include
#include
using namespace std;
class Cube
{
private:
double _length;
double _width;
double _height;
public:
// 只能在类内部声明静态成员变量!
static int cubeTotalNumber;
// 声明静态函数成员
static int giveMeCubeTotalNumber(void);
// 随便 再定义一个静态函数成员
static void printCubeTotalNumber(void);
// 声明构造函数带参数
Cube(double length,double width,double height);
// 得到体积
double giveMeResult();
};
// 只能在类的外部 使用::定义和初始化类的静态成员变量
int Cube::cubeTotalNumber = 0;
// 定义静态成员函数
int Cube::giveMeCubeTotalNumber(void)
{
Cube::printCubeTotalNumber();
return cubeTotalNumber;
}
// 定义静态成员函数
void Cube::printCubeTotalNumber(void)
{
cout << Cube::cubeTotalNumber << endl;
}
// 定义构造函数
Cube::Cube(double length,double width,double height)
{
_length = length;
_width = width;
_height = height;
// 构造时,对象数目++
cubeTotalNumber++;
}
// 定义获取体积的函数
double Cube::giveMeResult()
{
return _length * _width * _height;
}
int main ()
{
cout << Cube::giveMeCubeTotalNumber() << endl;
Cube cube_1(5,6,7);
cout << cube_1.cubeTotalNumber << endl;
Cube cube_2(1.3,1.4,520);
cout << cube_2.cubeTotalNumber << endl;
}
效果:
为什么类中静态(static)成员不能在类的定义内初始化?
1. 首先,我们需要回顾一下C++里面,关于linkage和separate compilation最重要的一条rule:
one definition rule!
这里面的其中重要的一条就是,一个object,在所有的translation units中,一共只能出现一次定义。
2.然后,再想想class的定义我们一般放在哪里?放在头文件里面。如果static variables的定义是放在class的定义里面的。
多个不同的文件include了这个header,会导致好几个不同的translation unit都定义了同一个object loopsaker::a
面向对象程序设计中最重要的一个概念是继承。
继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行时间的效果。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。
这个已有的类称为基类,新建的类称为派生类。
继承代表了 is a 关系。例如,哺乳动物是动物,狗是哺乳动物,动物是生物,因此,狗是动物,狗是生物,等等。
一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。
定义一个派生类,我们使用一个类派生列表来指定基类。
类派生列表以一个或多个基类命名,形式如下:
class derived-class: access-specifier base-class
其中,访问修饰符 access-specifier 是 public、protected 或 private 其中的一个,
base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private。
假设有一个基类 Shape,Rectangle 是它的派生类,如下所示:
#include
using namespace std;
// 基类
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Total area: 35
#include
#include
using namespace std;
// 基类
class Person
{
protected:
string _name;
public:
// 声明成员函数
void setName(string name);
};
void Person::setName(string name)
{
_name = name;
}
// 子类Girl 公开继承自Perosn
class Girl: public Person
{
public:
// 成员函数声明
string fetchName(void);
};
string Girl::fetchName(void)
{
return _name;
}
int main ()
{
Girl cuteGirl;
// 继承自父类的set方法
cuteGirl.setName("面码");
// // 调用自己定义的get方法
cout << cuteGirl.fetchName() << endl;
}
派生类可以访问基类中所有的非私有成员。
因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。
我们可以根据访问权限总结出不同的访问类型,如下所示:
访问 | public | protected | private |
---|---|---|---|
同一个类 | yes | yes | yes |
派生类 | yes | yes | no |
外部的类 | yes | no | no |
一个派生类继承了所有的基类方法,但下列情况除外:
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。
继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。
我们几乎不使用 protected 或 private 继承,通常使用 public 继承。
当使用不同类型的继承时,遵循以下几个规则:
多继承即一个子类可以有多个父类,它继承了多个父类的特性。
C++ 类可以从多个类继承成员,语法如下:
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
其中,访问修饰符 access 是 public、protected 或 private 其中的一个,用来修饰每个基类,各个基类之间用逗号分隔,如上所示。
现在让我们一起看看下面的实例:
#include
using namespace std;
// 基类 Shape
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 基类 PaintCost
class PaintCost
{
public:
int getCost(int area)
{
return area * 70;
}
};
// 派生类
class Rectangle: public Shape, public PaintCost
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
int area;
Rect.setWidth(5);
Rect.setHeight(7);
area = Rect.getArea();
// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;
// 输出总花费
cout << "Total paint cost: $" << Rect.getCost(area) << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Total area: 35
Total paint cost: $2450
多继承实例代码:
#include
#include
using namespace std;
// 基类1:日期类
class Date
{
private:
int _year,_month,_day;
public:
// 声明构造函数
Date(int year,int month,int day);
void setDate(int year,int month,int day);
void show();
};
// 定义构造函数
Date::Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
void Date::setDate(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
void Date::show()
{
cout << _year << "." << _month << "." << _day << " ";
}
// 基类2:时间类
class Time
{
private:
int _hour,_minute,_second;
public:
// 声明构造函数
Time(int hour,int minute,int second);
void setTime(int hour,int minute,int second);
void show();
};
// 定义构造函数
Time::Time(int hour,int minute,int second)
{
_hour = hour;
_minute = minute;
_second = second;
}
void Time::setTime(int hour,int minute,int second)
{
_hour = hour;
_minute = minute;
_second = second;
}
void Time::show(void)
{
cout << _hour << ":" << _minute << ":" << _second << endl;
}
// 子类
class DateTime:public Date,public Time
{
public:
// 初始化列表:核心代码
DateTime(int year,int month,int day,int hour,int minute,int second) : Date( year, month, day),Time( hour, minute, second){}
// 重载父类的方法
void show();
};
void DateTime::show()
{
Date::show();
Time::show();
}
int main ()
{
DateTime now(2018,05,20,17,20,18);
now.show();
// 调用父类的set函数
now.setDate(2020,05,20);
now.setTime(11,20,30);
now.show();
}
运行效果:
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
下面的实例中,基类 Shape 被派生为两个类,如下所示:
#include
using namespace std;
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Rectangle class area :" <<endl;
return (width * height);
}
};
class Triangle: public Shape{
public:
Triangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Triangle class area :" <<endl;
return (width * height / 2);
}
};
// 程序的主函数
int main( )
{
Shape *shape;
Rectangle rec(10,7);
Triangle tri(10,5);
// 存储矩形的地址
shape = &rec;
// 调用矩形的求面积函数 area
shape->area();
// 存储三角形的地址
shape = &tri;
// 调用三角形的求面积函数 area
shape->area();
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Parent class area
Parent class area
导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接 - 函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。
但现在,让我们对程序稍作修改,在 Shape 类中,area() 的函数声明前放置关键字 virtual,如下所示:
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
virtual int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};
修改后,当编译和执行前面的实例代码时,它会产生以下结果:
Rectangle class area
Triangle class area
此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在指针 *shape 中,所以会调用各自的 area() 函数。
正如您所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。
有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。
虚函数 是在基类中使用关键字 virtual 声明的函数。
在派生类中重新定义(覆盖)基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意时刻任何地点都可以根据所调用的对象类型来决定选择要调用的函数,这种操作被称为动态链接,或后期绑定。
您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,
但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
virtual 返回值 函数名() = 0;
我们可以把基类中的虚函数 area() 改写如下:
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// pure virtual function
virtual int area() = 0;
};
= 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数。
在父类中 尚未使用virtual关键字时的代码如下:
#include
#include
using namespace std;
class Person
{
public:
void showClassName();
};
void Person::showClassName(){
cout << "Person类" << endl;
}
class Girl:public Person
{
public:
void showClassName();
};
void Girl::showClassName(){
cout << "Girl类" << endl;
}
class Boya:public Person
{
public:
void showClassName();
};
void Boya::showClassName(){
cout << "Boya类" << endl;
}
int main ()
{
// 定义一个父类指针,让它指向不同的子类实例
Person *point_of_person;
Girl cuteGirl;
Boya boya;
// 让父类指针,指向不同子类实例
point_of_person = &cuteGirl;
point_of_person->showClassName();
// 让父类指针,指向不同子类实例
point_of_person = &boya;
point_of_person->showClassName();
}
运行效果如下:
#include
#include
using namespace std;
class Person
{
public:
// 使用virtual关键字修饰时
virtual void showClassName();
// 下面是纯虚函数
// virtual void showClassName() = 0;
};
void Person::showClassName(){
cout << "Person类" << endl;
}
class Girl:public Person
{
public:
void showClassName();
};
void Girl::showClassName(){
cout << "Girl类" << endl;
}
class Boya:public Person
{
public:
void showClassName();
};
void Boya::showClassName(){
cout << "Boya类" << endl;
}
int main ()
{
// 定义一个父类指针,让它指向不同的子类实例
Person *point_of_person;
Girl cuteGirl;
Boya boya;
// 让父类指针,指向不同子类实例
point_of_person = &cuteGirl;
point_of_person->showClassName();
// 让父类指针,指向不同子类实例
point_of_person = &boya;
point_of_person->showClassName();
}
数据抽象是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。
数据抽象是一种依赖于接口和实现分离的编程(设计)技术。
让我们举一个现实生活中的真实例子,比如一台电视机,您可以打开和关闭、切换频道、调整音量、添加外部组件(如喇叭、录像机、DVD 播放器),但是您不知道它的内部实现细节,也就是说,您并不知道它是如何通过缆线接收信号,如何转换信号,并最终显示在屏幕上。
因此,我们可以说电视把它的内部实现和外部接口分离开了,您无需知道它的内部实现原理,直接通过它的外部接口(比如电源按钮、遥控器、声量控制器)就可以操控电视。
现在,让我们言归正传,就 C++ 编程而言,C++ 类为数据抽象提供了可能。它们向外界提供了大量用于操作对象数据的公共方法,也就是说,外界实际上并不清楚类的内部实现。
例如,您的程序可以调用 sort() 函数,而不需要知道函数中排序数据所用到的算法。实际上,函数排序的底层实现会因库的版本不同而有所差异,只要接口不变,函数调用就可以照常工作。
在 C++ 中,我们使用类来定义我们自己的抽象数据类型(ADT)。
您可以使用类 ostream 的 cout 对象来输出数据 到标准输出,如下所示:
#include
using namespace std;
int main( )
{
cout << "Hello C++" <<endl;
return 0;
}
在这里,您不需要理解 cout 是如何在用户的屏幕上显示文本。您只需要知道公共接口即可,cout 的底层实现可以自由改变。
在 C++ 中,我们使用 访问标签 来定义类的抽象接口。一个类可以包含零个或多个访问标签:
访问标签 出现的频率没有限制。
每个访问标签 指定了紧随其后的成员定义的访问级别。
指定的访问级别会一直有效,直到遇到下一个访问标签或者遇到类主体的关闭右括号为止。
数据抽象有两个重要的优势:
如果只在类的私有部分定义数据成员,编写该类的作者就可以随意更改数据。
如果实现发生改变,则只需要检查类的代码,看看这个改变会导致哪些影响。
如果数据是公有的,则任何直接访问旧表示形式的数据成员的函数都可能受到影响。
C++ 程序中,任何带有公有和私有成员的类都可以作为数据抽象的实例。请看下面的实例:
#include
using namespace std;
class Adder{
public:
// 构造函数
Adder(int i = 0)
{
total = i;
}
// 对外的接口
void addNum(int number)
{
total += number;
}
// 对外的接口
int getTotal()
{
return total;
};
private:
// 对外隐藏的数据
int total;
};
int main( )
{
Adder a;
a.addNum(10);
a.addNum(20);
a.addNum(30);
cout << "Total " << a.getTotal() <<endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Total 60
上面的类把数字相加,并返回总和。公有成员 addNum 和 getTotal 是对外的接口,用户需要知道它们以便使用类。私有成员 total 是用户不需要了解的,但又是类能正常工作所必需的。
代码如下:
#include
#include
using namespace std;
class Girl
{
private:
int _candyTotalNumber;
public:
Girl(int initCanyNum);
// 对外接口
void receiveCandy(int candyNum);
int showAllCandy();
};
// 初始的糖果数目
Girl::Girl(int initCanyNum)
{
_candyTotalNumber = initCanyNum;
}
void Girl::receiveCandy(int candyNum)
{
_candyTotalNumber += candyNum;
}
int Girl::showAllCandy()
{
return _candyTotalNumber;
}
int main ()
{
// 初始妹纸有67颗糖
Girl loli(67);
// 后来又得到了520颗糖
loli.receiveCandy(520);
cout << loli.showAllCandy() << endl;
// 打印结果:587颗糖
}
抽象把代码分离为接口和实现。所以在设计组件时,必须保持接口独立于实现,这样,如果改变底层实现,接口也将保持不变。
在这种情况下,不管任何程序使用接口,接口都不会受到影响,只需要将最新的实现重新编译即可。
所有的 C++ 程序都有以下两个基本要素:
封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。
数据封装引申出了另一个重要的 OOP 概念,即数据隐藏。
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,
数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
C++ 通过创建类来支持封装和数据
我们已经知道,类包含私有成员(private)、保护成员(protected)和公有成员(public)成员。
默认情况下,在类中定义的所有项目都是私有的。例如:
class Box
{
public:
double getVolume(void)
{
return length * breadth * height;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
变量 length、breadth 和 height 都是私有的(private)。
这意味着它们只能被 Box 类中的其他成员访问,而不能被程序中其他部分访问。这是实现封装的一种方式。
为了使类中的成员变成公有的(即,程序中的其他部分也能访问),必须在这些成员前使用 public 关键字进行声明。
所有定义在 public 标识符后边的变量或函数可以被程序中所有其他的函数访问。
把一个类定义为另一个类的友元类,会暴露实现细节,从而降低了封装性。
理想的做法是尽可能地对外隐藏每个类的实现细节。(人亦如此!)
C++ 程序中,任何带有公有和私有成员的类都可以作为数据封装和数据抽象的实例。请看下面的实例:
#include
using namespace std;
class Adder{
public:
// 构造函数
Adder(int i = 0)
{
total = i;
}
// 对外的接口
void addNum(int number)
{
total += number;
}
// 对外的接口
int getTotal()
{
return total;
};
private:
// 对外隐藏的数据
int total;
};
int main( )
{
Adder a;
a.addNum(10);
a.addNum(20);
a.addNum(30);
cout << "Total " << a.getTotal() <<endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Total 60
上面的类把数字相加,并返回总和。公有成员 addNum 和 getTotal 是对外的接口,用户需要知道它们以便使用类。私有成员 total 是对外隐藏的,用户不需要了解它,但它又是类能正常工作所必需的。
通常情况下,我们都会设置类成员状态为私有(private),除非我们真的需要将其暴露,这样才能保证良好的封装性。
这通常应用于数据成员,但它同样适用于所有成员,包括使用了virtual关键字修饰的虚函数。
接口描述了类的行为和功能,而不需要完成类的特定实现。
C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。
如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。
纯虚函数是通过在声明中使用 "= 0" 来指定的,如下所示:
class Box
{
public:
// 纯虚函数
virtual double getVolume() = 0;
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
设计抽象类(通常称为 ABstract Class)的目的,是为了给其他类提供一个可以继承的适当的基类。
抽象类不能被用于实例化对象,它只能作为接口使用。
如果试图实例化一个抽象类的对象,会导致编译错误。
因此,如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支持使用 ABC 声明接口。
如果没有在派生类中重载纯虚函数,就尝试实例化该类的对象,会导致编译错误。
可用于实例化对象的类被称为具体类。
请看下面的实例,基类 Shape 提供了一个接口 getArea(),在两个派生类 Rectangle 和 Triangle 中分别实现了 getArea():
#include
using namespace std;
// 基类
class Shape
{
public:
// 提供接口框架的纯虚函数
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
}
};
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
// 输出对象的面积
cout << "Total Triangle area: " << Tri.getArea() << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Total Rectangle area: 35
Total Triangle area: 17
从上面的实例中,我们可以看到一个抽象类是如何定义一个接口 getArea(),两个派生类是如何通过不同的计算面积的算法来实现这个相同的函数。
面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。
然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。
外部应用程序提供的功能(即公有函数)在抽象基类中是以纯虚函数的形式存在的。这些纯虚函数在相应的派生类中被实现。
这个架构也使得新的应用程序可以很容易地被添加到系统中,即使是在系统被定义之后依然可以如此。