C++类 类 类继承 操作符重载 struct 多态

与java等面向对象语言类型类似,定义类需要定义访问修饰符、类成员与类函数。在类定义中定义的成员函数把函数声明为内联的,即便没有使用 inline 标识符。class定义的成员默认是 private 的。struct则是public,可以在外部定义成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码class Box
{
private:
double length; // 盒子的长度
double breadth; // 盒子的宽度
double height; // 盒子的高度
public:
// 成员函数声明
double get(void);
void set( double len, double bre, double hei )
// 构造函数
Box();
Box(double len, double bre, double hei):length(len),breadth(bre),height(hei);
~Box();
};
// 成员函数定义
double Box::get(void)
{
return length * breadth * height;
}

类的对象的公共数据成员可以使用直接成员访问运算符 . 来访问指针访问成员对象使用****

访问修饰符

类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的

  • public公有成员在程序中类的外部是可访问的
  • private私有成员变量或函数在类的外部是不可访问的,只有类和友元函数可以访问私有成员
  • protected(受保护)成员在派生类(即子类)中是可访问的

构造函数执行顺序

基本逻辑:基类构造→子类构造→子类析构→基类析构

构造函数执行顺序

  1. 基类构造函数。如果有多个基类,则构造函数的调用顺序是该基类在派生类中出现的顺序,而不是他们在成员初始化列表中的顺序
  2. 成员类对象构造函数。如果有多个成员类构造函数调用顺序是对象在类中被声明的顺序,而不是他们在成员初始化列表中的顺序
  3. 派生类构造函数

析构函数顺序

  1. 派生类析构函数
  2. 成员类对象的析构函数
  3. 调用基类的析构函数

构造函数

用于为属性赋值/初始化,与java的构造函数一样

1
2
3
4
5
ini复制代码// 成员函数定义,包括构造函数
Line::Line(double len, int a) {
length = len;
ac = a;
}

另一种写法是使用初始化列表来初始化字段,推荐使用这种方法初始化构造函数,直接把参数len赋值给length

1
2
3
4
css复制代码C::C( double a, double b, double c): X(a), Y(b), Z(c)
{
....
}

需要注意,C++ 初始化类成员时,是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序,顺序错误可能导致某些变量没有被赋值,因此变量顺序需要一致。

explicit 关键字

explicit 只对构造函数有效,用来避免隐式类型转换。且只对仅含有一个参数的类构造函数有效,因为多于两个的时候是不会发生隐式转换的(除非只有一个参数需要赋值,其他的参数有默认值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
arduino复制代码Class String{
String (int n); // 分配n个字节空间给字符串
String (const char* p); // 用字符串p的值初始化字符串
}

# 正常初始化方法
String s1(10); // 10个字节长度的字符串
String s2("Hello world!") // s2的初始值为 Hello world

# 隐式转换写法
String s3 = 10; // 编译通过,分配10个字节长度的字符串
String s4 = 'a'; // 编译通过,分配int('a')个字节长度的字符串
String s5 = "a"; // 编译通过,调用的是String (const char* p)

# 使用 explicit 关键字
Class String{
explicit String (int n); // 分配n个字节空间给字符串
String (const char* p); // 用字符串p的值初始化字符串
}

String s3 = 10; // 编译不通过,不允许隐式转换类型
String s4 = 'a'; // 编译不通过,不允许隐式转换类型

使用**explicit关键字能够在编译阶段给出错误,避免运行时错误,规范化构造函数的使用,尽量使用explicit**

初始化对象

c++使用了两种用构造函数来初始化对象的方式:

1
2
3
4
5
6
ini复制代码//显示的调用构造函数
Stock food = Stock("World Cabbage", 250, 1.25);
//隐式的调用构造函数
Stock garment("Furry Mason", 50, 2.5);
// 动态分配new 动态的创建一个Stock对象,并将对象的地址赋给pstock指针,用指针管理该对象
Stock *pstock = new Stock("Furry Mason", 50, 2.5);

默认构造函数是指在没有显示的赋值时,用来创建对象的构造函数。如Stock fluffyp; ,如果类没有构造函数,c++编译器将自动提供一个默认构造函数,用来创建对象但不初始化值,一般默认构造函数的形式如Stokc::Stock(){} ,类没有定义任何构造函数的时候,编译器才会提供默认构造函数。但是如果类定义了构造函数,就必须要写一个默认构造函数。

析构函数

类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

拷贝构造函数

拷贝构造函数是一种特殊的构造函数,它在创建对象时使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:

  • 一个对象以值传递的方式传入函数体
  • 一个对象以值传递的方式从函数返回
  • 一个对象需要通过另外一个对象进行初始化。

当类成员中含有指针类型成员且需要对其分配内存时,一定要有总定义拷贝构造函数,默认的拷贝构造函数实现的只能是浅拷贝,即直接将原对象的数据成员值依次复制给新对象中对应的数据成员,并没有为新对象另外分配内存资源。这样,如果对象的数据成员是指针,两个指针对象实际上指向的是同一块内存空间。在某些情况下,浅拷贝回带来数据安全方面的隐患。

当类的数据成员中有指针类型时,我们就必须定义一个特定的拷贝构造函数,该拷贝构造函数不仅可以实现原对象和新对象之间数据成员的拷贝,而且可以为新的对象分配单独的内存资源,这就是**深拷贝构造函数**。

如果在类中没有定义拷贝构造函数,编译器会自行定义一个如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。

1
2
3
arduino复制代码classname (const classname &obj) {
// 构造函数的主体
}
1
2
3
4
5
ini复制代码Line::Line(const Line &obj)
{
ptr = new int;
*ptr = *obj.ptr; // 拷贝值
}

友元函数

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
arduino复制代码class Box
{
double width;
public:
friend void printWidth(Box box); // 声明友元函数
friend class BigBox; // 声明自己的友元类
void setWidth(double wid);
};

class BigBox
{
public :
void Print(int width, Box &box)
{
box.setWidth(width); // BigBox是Box的友元类,它可以直接访问Box类的任何成员
}
};

// 请注意:printWidth() 不是任何类的成员函数
void printWidth(Box box)
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "Width of box : " << box.width << endl;
}

类指针

一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 -> ,就像访问指向结构的指针一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c复制代码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;
}

类作用域与静态成员

在类中声明常量要注意,在类中类只是描述一个结构的形式,在没有创建对象之前,类是没有用于存储的空间的,因此不能如下直接在类中声明一个变量值:

1
2
3
4
arduino复制代码class temp{
private:
const int Months = 12;//这样是不行的
};

如果我们要声明一个变量,要在前面加上static关键字,表明该常量是与其他静态变量存储在一起的,而不是存储在对象中的。因此month可以被所有的对象共享,在类声明时不能初始化static,但可以加上const初始化一个静态字面值常量。

类继承

在java中叫做子类,在c++中叫做派生类

1
2
3
4
5
6
7
8
9
kotlin复制代码// 基类
class Animal {
// eat() 函数
// sleep() 函数
};
//派生类
class Dog : public Animal {
// bark() 函数
};

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

多继承

多继承即一个子类可以有多个父类,它继承了多个父类的特性。

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码// 基类 Shape
class Shape
{};

// 基类 PaintCost
class PaintCost
{};

// 派生类
class Rectangle: public Shape, public PaintCost
{};

操作符重载

操作符重载,也叫运算符重载,是C++的重要组成部分,如,加法运算符“+”对整数、单精度数和双精度数的操作是大不相同的。这是因为C++语言本身已经重载了该运算符,所以它能够用于int、float、double和其它内部定义类型的变量。操作符重载可对已有的运算符(C++中预定义的运算符)赋予多重的含义,使同一运算符作用于不同类型的数据时导致不同类型的行为。

其目的是:扩展C++中提供的运算符的适用范围,以用于类所表示的抽象数据类型。同一个运算符,对不同类型的操作数,所发生的行为不同。

1
2
3
4
5
6
arduino复制代码函数类型 operator 运算符名称 (形参表列)
{
对运算符的重载处理
}
// 例
Complex operator+ (Complex& c1,Complex& c2);

operator是C++的关键字,专门用于定义重载运算符的函数。operator +就是函数名,表示对运算符+重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arduino复制代码class Complex
{
public:
Complex( double = 0.0, double = 0.0 );
Complex operator+( const Complex & ) const;
Complex operator-( const Complex & ) const;
private:
double real; // real part
double imaginary; // imaginary part
};
Complex Complex::operator+( const Complex &operand2 ) const
{
return Complex( real + operand2.real, imaginary + operand2.imaginary );
}
Complex Complex::operator-( const Complex &operand2 ) const
{
return Complex( real - operand2.real, imaginary - operand2.imaginary );
}

实质操作符的重载就是函数的重载,在程序编译时把指定的运算表达式转换成对运算符的调用,把运算的操作数转换成运算符函数的参数,根据实参的类型决定调用哪个操作符函数。如c3 = c1+c2最后在C++编译系统中被解释为:c3=c1.operator+(c2)

对于单目运算符++和–有两种使用方式,前置运算和后置运算,它们是不同的。针对这一特性,C++约定:如果在自增(自减)运算符重载函数中,无参数表示前置运算符函数,若加一个int型形参,就表示后置运算符函数。有一个Time类,包含数据成员minute(分)和sec(秒),模拟秒表,每次走一秒,满60秒进一分钟,此时秒又从0开始算。要求输出分和秒的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
arduino复制代码class Time
{
public:
Time( ){minute=0;sec=0;}
Time(int m,int s):minute(m),sec(s){ }
Time operator++( ); //声明前置自增运算符“++”重载函数
Time operator++(int); //声明后置自增运算符“++”重载函数
private:
int minute;
int sec;
};
Time Time::operator++( ) //定义前置自增运算符“++”重载函数
{
if(++sec>=60)
{
sec-=60; //满60秒进1分钟
++minute;
}
return *this; //返回当前对象值
}
Time Time::operator++(int) //定义后置自增运算符“++”重载函数
{
Time temp(*this);
sec++;
if(sec>=60)
{
sec-=60;
++minute;
}
return temp; //返回的是自加前的对象
}

struct

总的来说,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。最本质的区别就是默认的访问控制,structpublic的,classprivate 的。

struct重载运算符

1
2
3
4
5
6
7
8
9
csharp复制代码struct Inf
{
int hh, mm, ss;
string inOrOut;
Inf(){}
Inf(int hh, int mm, int ss) :hh(hh), mm(mm), ss(ss){}
}
# bool为函数类型,operator<一起为函数的名字(const Inf r)定义了一个不可以修改的参数
bool operator< (const Inf r)const { return hh * 3600 + mm * 60 + ss < r.hh * 3600 + r.mm * 60 + r.ss; }

多态

多态是在不同继承关系的类对象,去调同一函数,产生了不同的行为。在c++中多态有两种表现方式:

  • 「派生类的指针」可以赋给「基类指针」;调用哪个虚函数,取决于指针对象指向哪种类型的对象
  • 派生类的对象可以赋给基类「引用」;调用哪个虚函数,取决于引用的对象是哪种类型的对象

虚函数

c++使用虚函数实现多态,虚函数是一种由virtual关键字修饰的一种类内函数,可分为虚函数和纯虚函数。与java不同的是java中使用接口声明函数,父类与子类实现接口,C++在父类与子类中同时声明虚函数并实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
csharp复制代码// 基类
class CFather
{
public:
virtual void Fun() { } // 虚函数
};

// 派生类
class CSon : public CFather
{
public :
virtual void Fun() { }
};

int main()
{
CSon son;
CFather *p = &son;
p->Fun(); //调用哪个虚函数取决于 p 指向哪种类型的对象
// p 指针对象指向的是 CSon 类对象,所以 p->Fun() 调用的是 CSon 类里的 Fun 成员函数。
return 0;
}

析构函数可以写成虚的,但是构造函数不行。其中的原因比较复杂,简单地来说就是虚函数是通过一种特殊的功能来实现的,它储存在类所在的内存空间中,构造函数一般用于申请内存,那连内存都没有,怎么能找到这种特殊的功能呢?所以构造函数不能是虚的。

虚函数中的this指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
csharp复制代码class Base 
{
public:
void fun1()
{
this->fun2();
}

virtual void fun2() // 虚函数
{
cout << "Base::fun2()" << endl;
}
};

class Derived : public Base
{
public:
virtual void fun2() // 虚函数
{
cout << "Derived:fun2()" << endl;
}
};

int main()
{
Derived d;
Base *pBase = & d;
pBase->fun1();//Derived:fun2()
return 0;
}

this 指针的作用就是指向成员函数所作用的对象, 所以非静态成员函数中可以直接使用 this 来代表指向该函数作用的对象的指针。

pBase 指针对象指向的是派生类对象,派生类里没有 fun1 成员函数,所以就会调用基类的 fun1 成员函数,在Base::fun1() 成员函数体里执行 this->fun2() 时,实际上指向的是派生类对象的 fun2 成员函数。正确的输出结果是Derived:fun2()

析构函数最好是虚函数

基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。这里他们的函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,这也说明的基类的析构函数最好写成虚函数。

将析构函数定义为虚函数的原因

  • 基类指针可能指向派生类,当delete的时候,如果不定为虚函数,系统会直接调用基类的析构函数,这个时候派生类就有一部分没有被释放,就会造成可怕的内存泄漏问题。
  • 若定义为虚函数构成多态,那么就会先调用派生类的析构函数然后派生类的析构函数会自动调用基类的析构函数,这个结果满足我们的本意。

所以,在继承的时候,尽量把基类的析构函数定义为虚函数,这样继承下去的派生类的析构函数也会被变成虚函数构成多态。把基类的析构函数声明为virtual,派生类的析构函数可以 virtual 不进行声明,通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数

纯虚函数

在虚函数的后面写上 =0 ,则这个函数就变成纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。这个纯虚函数的作用就是强迫我们重写虚函数,构成多态。这样更加体现出了接口继承。类似于Java中接口的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
arduino复制代码#include <iostream>

class Person
{
public:
virtual void Strength() = 0;
};

class Adult : public Person
{
public:
virtual void Strength()
{
std::cout << "Adult have big Strength!" << std::endl;
}
};

class Child : public Person
{
public:
virtual void Strength()
{
std::cout << "Child have Small Strength!" << std::endl;
}
};

在单继承的前提下,只要实例化的派生类不是抽象类就可以,一个抽象类是可以继承自抽象类的,并且它可以被另一个类所继承。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%