2.类
类的定义
公有继承的特点:基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被派生类访问。
私有继承的特点:将基类的公有成员和保护成员变成派生类自身的私有成员,而基类的私有成员在派生类中本身就不能访问。
保护继承的特点:基类的所有公有成员和保护成员都可作为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
头文件
大部分的库包含众多的函数和变量。为了减少工作量,确保一致性,当对这些函数和变量做外部声明时,C/C++使用“头文件”(header file)。
头文件是一个含有某个库的外部声明函数和变量的文件。
它通常是扩展名为“.h”的文件,如headerfile.h(可能还会看到一些较老的程序使用其他扩展名,如“.hxx”或“.hpp”,但现在已经很少了)。
首先要在头文件中进行声明:
#ifndef UNTITLED_STOCK_H
#define UNTITLED_STOCK_H
#include <string>
class Stock {
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_toto();
public:
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
Stock(const std::string &co, long n, double pr);
};
inline void Stock::set_toto() {
total_val = shares * share_val;
}
#endif //UNTITLED_STOCK_H
然后要在.cpp文件中进行实现:
#include <iostream>
#include "Stock.h"
Stock::Stock(const std::string &co, long n, double pr) {
company = co;
if (n < 0) {
std::cout << "Number can < 0";
shares = 0;
} else {
shares = n;
}
share_val = pr;
set_toto();
}
void Stock::buy(long num, double price) {
if (num < 0) {
std::cout << "can not < 0";
} else {
shares += num;
share_val = price;
set_toto();
}
}
void Stock::sell(long num, double price) {
using std::cout;
if (num < 0) {
cout << "can < 0";
} else if (num > shares) {
cout << "more than you have";
} else {
shares -= num;
share_val = price;
set_toto();
}
}
void Stock::update(double price) {
share_val = price;
set_toto();
}
void Stock::show() {
std::cout << "show ....";
}
定义成员函数时,使用作用域解析运算符(::)来表示函数所属的类。
这意味着我们定义的show()函数是Stock类的成员。 因此,作用域解析运算符确定了方法定义对应的类的身份。
使用类:
Stock food = Stock("world", 250, 2.5);
Stock garment("Furry", 50, 2.5); // 与上面等价
food.show();
static修饰成员变量
- 静态数据成员不属于某个对象,在为对象分配空间中不包括静态成员所占空间。
- 静态变量,是在编译阶段就分配空间,对象还没有创建时,就已经分配空间。
- 静态成员变量必须在类中声明,在类外定义。
Data.h
class Data {
private:
int num;
char c;
public:
static int a1;
static int b;
};
Data.cpp
#include "Data.h"
int Data::a1 = 0;
int Data::b = 0;
main.cpp
using namespace std;
int main() {
cout << sizeof(Data) << endl;
}
这里Data的大小是8,为什么是8呢?
这是因为内存对齐。
需要内存对齐的原因:
1,32位系统cpu一次寻址4个字节,64位系统一次寻址8个字节
以64位系统为列:CPU一次寻址都是从地址为8的倍数的地址开始寻址,如果存储的数据都是内存对齐的,即每一个数据的首地址也都是存放在以8为倍数的地址中,那么CPU一次寻址就可以读取整个数据。
如果没有内存对齐,数据都是一个接着一个存储的。那么为了访问未对齐的数据的内存,CPU处理器一次读取的内存数据(8字节)可能包含两个数据的存储数据,而导致数据读取不完整,需要作两次内存访问;而对齐的内存访问仅需要一次访问。
- 为了可以在不同平台上兼容。
基本数据类型的内存对齐: 计算机系统对基本类型数据在内存中放的位置做了限制,它们会要求这些数据的首地址是一个数(一般为4---32位系统或者8---64位系统)的整数倍,也就是说这些数据的首地址被放到4/8的整数倍的地址中。
默认构造函数
默认构造函数是在未提供显示初始值时,用来创建对象的构造函数。也就是说,它用于下面这种声明的构造函数:
Stock cat; // 使用默认构造函数
这条语句的管用的原因在于,如果没有提供任何构造函数,则C++将自动提供默认构造函数。它是默认构造函数的隐式版本,不做任何工作。 对于Stock类来说,默认构造函数可能如下:
Stock::Stock() {}
奇怪的是,当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。
为类定义了构造函数后,程序员就必须为它提供默认构造函数。
如果提供了非默认构造函数,但没有提供默认构造函数,则下面的声明就会报错:
Stock sotck; // 报错
explicit关键字
在C++编程语言中,explicit是一个关键字,用于修饰类的单参数构造函数。
它的含义是指定该构造函数不能被用于隐式类型转换。
换句话说,使用了explicit关键字的构造函数只能显式地调用,不能在隐式转换的上下文中自动调用。
默认情况下,C++编译器可以自动进行一些类型转换,例如将一个参数的类型自动转换为构造函数所需的类型。
这种隐式类型转换有时会导致意外的结果或者引发编程错误。
通过使用explicit关键字,可以禁止这种隐式转换,从而增加代码的可读性和安全性。
下面是一个使用explicit关键字的示例:
class MyInt {
public:
explicit MyInt(int value) : num(value) {}
int getValue() const { return num; }
private:
int num;
};
void printInt(const MyInt& num) {
std::cout << num.getValue() << std::endl;
}
int main() {
MyInt num1 = 5; // 错误!不能进行隐式类型转换
MyInt num2(10); // 正确!显式调用构造函数
printInt(num2); // 正确!显式调用构造函数
return 0;
}
在上述示例中,MyInt类有一个带有explicit关键字的单参数构造函数,该构造函数禁止了隐式类型转换。
因此,在main()函数中,不能将整数直接隐式转换为MyInt对象,而是需要显式地调用构造函数。
总而言之,explicit关键字的含义是指定一个构造函数不能在隐式类型转换的上下文中自动调用,只能通过显式调用来创建对象。
这有助于提高代码的可读性和可靠性。
析构函数
用构造函数创建对象后,程序负责跟踪对象,直到其过期为止。对象过期后,程序将自动调用一个特殊的成员函数,该函数的名称令人生畏-析构函数。
析构函数完成清理工作,因此实际上很有用。
例如,如果构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存。 上面Stock的构造函数如果没有使用new,因此析构函数实际上没有需要完成的任务。在这种情况下,只需让编译器生成一个什么都不做的析构函数即可。
继承
- Student.hpp
#ifndef Student_hpp
#define Student_hpp
#include <stdio.h>
#include <iostream>
using namespace std;
class Student {
public:
Student(string sn, int ia, float fs);
void dis();
private:
string name;
int age;
float sscore;
};
#endif /* Student_hpp */
- Student.cpp
#include "Student.hpp"
Student::Student(string sn, int ia, float fs)
:name(sn), age(ia), sscore(fs) {
}
void Student::dis() {
cout << "name" << name << endl;
}
- Graduate.hpp
#ifndef Graduate_hpp
#define Graduate_hpp
#include <stdio.h>
#include "Student.hpp"
using namespace std;
class Graduate : public Student {
public:
Graduate(string sn, int ia, float fs, double ds);
void print();
private:
double salary;
};
#endif /* Graduate_hpp */
- Graduate.cpp
#include "Graduate.hpp"
// 构造函数中调用父类构造函数,对于特有的属性调用自己的初始化方法
Graduate::Graduate(string sn, int ia, float fs, double ds)
: Student(sn, ia, fs), salary(ds){
}
void Graduate::print() {
dis();
cout << "salary" << salary << endl;
}
- main.cpp
#include <iostream>
#include "Student.hpp"
#include "Graduate.hpp"
using namespace std;
int main(int argc, const char * argv[]) {
Student s("zhaosi", 32, 120);
s.dis();
Graduate g("nengge", 35, 105, 200.0);
g.print();
return 0;
}
结构体
联合类型(union)与结构类型(struct)无论是在定义方法还是成员存取上都十分相似,但结构类型所定义的每个成员拥有各自的内存空间,而联合却是共享内存空间。联合变量内的各个成员以同一个内存区块来存储数据,并以占最大内存空间的成员作为联合类型的内存空间大小。
类的概念其实是由C语言的结构类型衍生而来的,二者的差别在于结构类型只能包含数据变量,而类类型(class type,也可简称为类)可扩充到包含处理数据的函数。
抽象类
虚拟函数(Virtual Function)就是多态的实现,使得我们能够调用相同的函数却执行不同的运算,因为这个成员函数所属的类实例可以被动态链接,而这些派生类又具有相同的基类。
要在C++中创建虚拟函数,可以直接使用关键字“virtual”来声明,即表示该函数为虚拟函数。一旦将函数声明为虚拟函数,就必须在派生类中重载该虚拟函数。
另外,派生类虚拟函数的参数与返回值还必须与基类中声明的虚拟函数相同。一旦将函数声明为虚拟函数,编译程序就会给予这些函数不同的指针,在执行时则根据这些指针来存取适当的函数。
所以当您要声明对象时,必须同时声明指针变量。在声明虚拟函数时,在表达式的尾端加入语句“=0”,而不加入任何定义该函数功能的语句,这种虚拟函数被称为“纯虚拟函数”(pure virtual function)。
纯虚拟函数最主要的功能是在起始声明时并未加以定义该虚拟函数的本质而形成一种被保留的函数接口(interface)。
纯虚拟函数无法在单一类或是派生类中声明,只能存在于拥有继承关系的基类中,这种基类称为“抽象基类”(abstract class)。
抽象基类包含了最少一个或多个纯虚拟函数,所以当派生类继承了抽象基类之后,必须在派生类中“重新定义”(override,或者覆盖)及“实现”(implement)所继承的纯虚拟函数。
含有纯虚函数的类就是Java中的接口。是不能实例化的。
含有虚函数的类,析构函数也应该声明为虚函数。
这样在delete父类指针的时候,才会调用子类的析构函数,实现完整析构。
实现多态性:
在C++语言中实现多态性,需在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
在 C++ 中,虚函数的实现原理基于两个关键概念:虚函数表和虚函数指针。
虚函数表:每个包含虚函数的类都会生成一个虚函数表(Virtual Table),其中存储着该类中所有虚函数的地址。
虚函数表是一个由指针构成的数组,每个指针指向一个虚函数的实现代码。
虚函数指针:在对象的内存布局中,编译器会添加一个额外的指针,称为虚函数指针或虚表指针(Virtual Table Pointer,简称 VTable 指针)。
这个指针指向该对象对应的虚函数表,从而让程序能够动态地调用正确的虚函数。
当一个基类指针或引用调用虚函数时,编译器会使用虚表指针来查找该对象对应的虚函数表,并根据函数在虚函数表中的位置来调用正确的虚函数。
虚析构
Animal *base = new Cat();
如果使用默认构造函数的时候,执行完之后会发现,调用了父类的构造函数,然后接着调用了子类的构造函数,但是执行完成后只调用了父类的析构函数,并没有调用子类的析构函数。
这是因为base指向的类型是Animal,在没有设计虚析构的时候,只能调用父类的析构函数。
为了解决这个问题就要用虚析构。
虚析构的作用: 通过基类指针、引用释放子类的所有空间。
虚析构: 在虚析构函数前加上virtual修饰。
纯虚析构
class B {
public:
// virtual修饰,并加上0
virtual ~B() = 0;
};
// 纯虚析构必须实现析构函数的函数体
B::~B() {
}
原因:通过基类指针释放了类对象时,会先调用子类析构,再调用父类析构(如果父类的析构不实现,那就无法实现调用)
模板
模板(可以理解为java的泛型)是实现代码重用机制的一种工具,它可以实现参数类型化,即把参数定义为类型,从而实现代码的可重用性。同时,模板能够减少源代码量并提高代码的机动性,却不会降低类型安全。
在C++语言中模板有两种,分别为“函数模板”(Function Template)与“类模板”(Class Template)。模板就是把功能相似、仅数据类型不同的函数或类设计为通用函数模板或类模板,提供给用户。
模板是“泛型编程”的基础,所谓泛型编程就是用独立于任何特定类型的方式编写代码。所以简单的说,类是对象的抽象,而模板又是类的抽象,用模板能定义出具体类。
基本上,无论是函数模板还是类模板的设计,都是把相同程序代码的函数集中编写在一起,而对于函数模板或类模板中的数据类型部分,则以模板形式参数(Template Formal Parameter)来替代。
当程序调用函数模板或类模板时,会根据传递参数的数据类型,把模板形式参数替换成该参数的数据类型,以创建此函数模板或类模板的程序实例(Instance)。
在设计类时,将数据类型以模板参数取代,在使用时再指定数据类型,这种类就称为类模板(Class Template)。在程序中,将会根据声明对象时所指定的数据类型来创建适用该数据类型的类。类模板的声明格式如下:
template <class或typename T> 返回类型 函数名(形式参数表) {
语句序列;
}
关键字class和typename有什么区别?
在模板定义中,关键字typename和class意义相同,可以互换使用,甚至可以在同一个模板形参表中同时存在。
但关键字typename是作为标准c++的组成部分加入到c++中的,因此旧的程序更有可能只用关键字class。
类模板的声明格式与函数模板类似,而声明的模板形式参数可以运用于类内的数据成员与成员函数。
另外,若是类成员函数定义在类外部,则必须附加其类模板声明与范围解析运算符(::),如下所示:
template <class模板形式参数>
class类名称
{
模板参数 函数名称();
}
template <class模板形式参数,……>
函数类型 类名称<模板参数>::函数名称()
{
//程序语句
}
声明完类模板后,就要用类模板来产生对象,对象的产生方式如下:
类名称<数据类型> 对象名称; // 声明对象
或
类名称<数据类型> 对象名称(); // 对象名称、对象和构造函数的合并声明
类和结构体的区别
- 结构体是成员默认为public的class
- 类的成员默认是private
写代码的时候尽量不要将class和struct混用,否则代码会显得比较混乱。
当然,如果把没有成员函数只有成员变量的数据结构定义为struct,而把有成员函数的数据结构定义为class也是可以的,那就在编写代码中一致遵循这个规则。
总之,有一个共同遵循的标准,而不是随意混用struct和class就好。
- 邮箱 :charon.chui@gmail.com
- Good Luck!