【C++】C++中类的基本使用
1.类、成员的声明,定义,初始化的基本规则
C++中类的基本模板如下:
namespace 空间命名{//可以定义namespace,也可以不定义 class/struct 类名称{
public/private: 基本成员; 构造函数():成员初始化列表{ 构造函数主体; }
返回类型 函数名称(参数列表) 修饰符{
函数主体;
} }; }
例如:
//Person.h #pragma once #include <string> using namespace std; class Person{ private://定义私有成员 string name; int age; public://定义公共成员 Person() = default;//c++11标准中使用=default,定义构造函数的默认行为 Person(string nm,int ag) : name(nm),age(ag){}//带两个参数的构造函数 Person(string nm) : name(nm){}//带一个参数的构造函数 Person(int ag) : age(ag){} /* * 或者写成: * Person(string nm) : Person(nm,0){}//调用带两个参数的构造函数,初始化age为0 * Person(int ag) : Person("",ag){}//调用有两个参数的构造函数,初始化name为"" * */ void setName(string name){ this->name = name; } string getName(){ return this->name; } void setAge(int age){ this->age = age; } int getAge(){ return this->age; } }; //PersonTest.cpp #include "Person.h" #include <iostream> using namespace std; int main(int argc, char * argv[]){ Person person("jamy",20), *p = &person; //也可以写成:Person person = Person("jamy",20); cout << "name:" << p->getName() << "\n" << "age:" << p->getAge() << endl; return 0; }
在定义类的时候,可以使用class关键字或struct关键字。这种变化仅仅是形式上有所不同,实际上我们可以使用这两个关键字中的任何一个定义类。唯一的区别是struct和class的默认访问权限不太一样。如果我们使用struct关键字,则定义在第一个访问说明符之前的成员是public的;相反,如果我们使用class关键字,则这些成员是private的。
出于统一编程的考虑,当我们希望定义的类的所有成员是public的时,使用struct;反之,如果希望成员是private的,使用class。
构造函数的名字和类名相同。和其他函数不一样的是,构造函数没有返回类型;构造函数也有一个(可能为空的)参数列表和一个(可能为空的)函数体。构造函数的初始值是成员名字的一个列表,每个名字后面紧跟括起来的成员初始值,不同成员的初始值通过逗号分隔开来。例如上面的:
Person(string nm,int ag) : name(nm),age(ag){}
其中name(nm),age(ag){},表示初始name成员为nm值,初始age成员为ag值,函数体为空。在函数体中,也可以改变参数的值:
Person(string nm,int ag){ this->name = nm;//赋值,并非初始化 this->age = ag;//赋值,并非初始化 }
但上面这段代码并没有初始化name和age值,他们只是重新修改name和age的值。并且有些特殊成员(例如引用和constexpr)是不运行这种方式的,所以建议使用初始化的方式。
2.this使用的注意点
需要注意在C++中,this不是代表本类对象,而是指向本类对象的指针。在使用的需要注意,this是一个指针。
成员函数通过一个名为this的额外隐式参数来访问调用它的那个对象,当我们调用一个成员函数时,用请求该函数的对象地址初始化this。
例如:
person.getName();
则编译器负责把person的地址传递给person的隐式形参this,可以隐式的认为编译器将该调用重写成了如下的形式:
Person::getName(&person);
其中调用Person的getName成员传入了person对象的地址。
默认情况下,this的类型是指向类类型非常量版本的的常量指针。在Person的类型中,this的默认类型是Person *const。
this的默认类型是Person *const,那么有没有其它的类型呢?答案是肯定的,当我们在定义函数的时候指定const关键字,那么this就是指向类类型常量版本的常量指针,在Person类中也就是 const Person * const类型。
例如:
void getName() const{//只能取值,不能修改调用对象的属性值 return this->name;//this的类型是const Person * const }
3.静态成员
和其他成员函数一样,我们既可以在类的内部也可以在类的外部定义静态成员函数。当在类的外部定义静态成员函数时,不能重复static关键字,该关键字只出现在类内部的声明语句。
因为静态数据成员不属于类的任何一个对象,所在它们并不是在创建类的对象时被定义的,这意味着它们不是由类的构造函数初始化的。而且一般来说,我们不能在类的内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员。但如果给静态成员加上constexpr,那么就可以在类内初始化了。
在类外的定义中,定义静态数据成员的方式和定义成员函数差不多,需要指定对象的类型名,类名,作用域运算符以及成员函数的名字,
在类外初始化静态成员的格式如下:
数据类型 类名::静态成员名 = 初始值
//Account.h class Account{ public: static double rate(){return interestRate;} static void rate(double); private: static double interestRate;//声明,不能有初始值,不能在类内部初始化 static 成员 static constexpr int period = 30;//可以在类内部初始化 static constexpr 成员 }; //Account.cpp #include "Account.h" double Account::interestRate = rate();//初始化静态成员interestRate,不能再次使用static关键字。这里等同于double Account::interestRate = interestRate。经过interestRate的赋值后,interestRate的值就是0. void Account::rate(double newRate){//初始化静态函数rate,不能再次使用static关键字 interestRate = newRate; }
上面案例中,Account::interestRate = rate()语句相当于Account::interestRate = interestRate,当静态变量定义后就会分配内存空间。这里interestRate是int类型,所以默认值为0。
下面的案例有比较清晰的解释:
#include <stdio.h> class A { public: static int a; //声明但未定义 }; int main() { printf("%d", A::a); return 0; }
编译以上代码会出现“对‘A::a’未定义的引用”错误。这是因为静态成员变量a未定义,也就是还没有分配内存,显然是不可以访问的。
#include <stdio.h> class A { public: static int a; //声明但未定义 }; int A::a = 3; //定义了静态成员变量,同时初始化。也可以写"int A:a;",即不给初值,同样可以通过编译 int main() { printf("%d", A::a); return 0; }
这样就对了,因为给a分配了内存,所以可以访问静态成员变量a了。
注意:在类外面 这样子写 :int A::a ; 也可以表示已经定义了。
4.类的继承
通过继承(inheritance)联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类(base class),其他类则直接或间接的从基类继承而来,这些继承得到的类被称为派生类(derived class)。
使用冒号(:)表示类的继承,语法格式为:
class DerivedClass : public BaseClass{}
访问修饰符
上面的DerivedClass使用public的访问修饰符可以访问BaseClass中的所有成员,这里需要注意,这里的public和成员前面的访问修饰符不是一个含义。
上面DerivedClass可以访问BaseClass中的所有成员(因为使用了pubic),如果使用了private的话,那么DerivedClass不能访问BaseClass中的任何成员(包括public成员也不可以)。
DerivedClass可以访问BaseClass中的所有成员,但这并不代表DerivedClass的派生类也可以完全访问DerivedClass的成员。这取决于DerivedClass的派生类在访问DerivedClass所使用的修饰符。
class DerivedClass2 : private DerivedClass{}
DerviedClass2不能访问DerivedClass的任何成员。
virtual函数
被virtual修饰的函数被称为虚函数,c++中虚函数所在的类也可以声明实例,只有纯虚函数所在类才不可以声明实例(后面会讲到的)。
使用virtual的函数是设计者希望各个派生类定义适合自身的版本。
class Shape{ private: virtual std::string name(){ return "i am a shape"; } } class Square : public Shape{ private: std::string name() override{ return "i am a square"; } }
final关键字
如果不希望发生继承,那么就可以使用final关键字,final关键字表示当前类是最终类,不能再有任何派生类。
class Square final{}
或者
class Square final : public Shape{}
5.定义抽象类
上面的实例中Shape和Square都可以声明实例,那么有没有什么限制只能让Square声明实例呢?答案是肯定的。使用纯虚函数(pure virtual)从而令程序实现我们设计的目的。
我们在函数体的位置(在声明语句的分号之前)书写=0就可以声明一个纯虚函数。=0只能出现在类内部的虚函数声明语句处:
class Shape{ private: std::string name() = 0; } //Shape();错误,不能声明Shape的实例
6.对象的多态性
继承主要作用就是多态。c++中要实现多态,必需要使用指针(推荐使用智能指针)。使用普通类是无法实现多态的。例如:
class Shape{ public: virtual std::string name() { return "I am a shape"; } }; class Square : public Shape{ public: std::string name() override { return "I am a square"; } };
我们首先使用普通类的方式指定一个Shape指向一个Square的实列对象。
Shape s = Square();
std::cout << s.name() << std::endl;
输出结果:
I am a Shape
我们期望的输出是“I am a Square”,但实际的输出是"I am a Shape"。这样显然没有多态的效果,如果我们换成shared_ptr的话(普通指针都可以,为了减少资源回收带来的复杂度,推荐使用智能智能),那么就解决这个问题:
std::shared_ptr<Shape> s = std::make_shared<Square>(Square());
std::cout << s->name()<< std::endl;
输出结果:
I am a Square
7.继承类中构造函数与析构函数的调用规则
在派生类中构造类的实例时,有义务对所有基类的成员完成初始化操作。
例如:
class Shape{ private: std::string name; public: Shape(std::string name):name(name){std::cout << "in shape constructor" << std::endl;} }; class Square : public Shape{ private: long width; public: Square(std::string name,long wd):Shape(name),width(wd){std::cout << "in square constructor" << std::endl;}; };
我们在Square的构造函数中调用了Shape(name)初始化基类的成员。
当初始化一个Square时候,输出:
in shape constructor in square constructor
当然,如果我们不在Square中调用Shape的构造函数,那么Shape的默认构造函数就会被调用。
从上面的输出结果可知,构造函数的首先从基类开始构造,再是派生类。而析构函数恰好和构造函数相反,先是派生类,再是基类。
struct Shape{ ~Shape(){std::cout << "shape destructor" << std::endl;} }; struct Square : public Shape{ ~Square(){std::cout << "square destructor" << std::endl;} };
接下来构造一个Square对象,再销毁:
{
Square();
}
输出:
square destructor
shape destructor
8. 多重继承
在前面的例子中,派生类都只有一个基类,称为单继承(Single Inheritance)。除此之外,C++也支持多继承(Multiple Inheritance),即一个派生类可以有两个或多个基类。
多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用,后来的 Java、C#、PHP 等干脆取消了多继承。
多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类D:
class D: public A, private B, protected C{ //类D新增加的成员 }
D 是多继承形式的派生类,它以公有的方式继承 A 类,以私有的方式继承 B 类,以保护的方式继承 C 类。D 根据不同的继承方式获取 A、B、C 中的成员,确定它们在派生类中的访问权限。
多继承下的构造函数
多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。以上面的 A、B、C、D 类为例,D 类构造函数的写法为:
D(形参列表): A(实参列表), B(实参列表), C(实参列表){ //其他操作 }
基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。仍然以上面的 A、B、C、D 类为例,即使将 D 类构造函数写作下面的形式:
D(形参列表): B(实参列表), C(实参列表), A(实参列表){ //其他操作 }
那么也是先调用 A 类的构造函数,再调用 B 类构造函数,最后调用 C 类构造函数。
下面是一个多继承的实例:
#include <iostream> using namespace std; //基类 class BaseA{ public: BaseA(int a, int b); ~BaseA(); protected: int m_a; int m_b; }; BaseA::BaseA(int a, int b): m_a(a), m_b(b){ cout<<"BaseA constructor"<<endl; } BaseA::~BaseA(){ cout<<"BaseA destructor"<<endl; } //基类 class BaseB{ public: BaseB(int c, int d); ~BaseB(); protected: int m_c; int m_d; }; BaseB::BaseB(int c, int d): m_c(c), m_d(d){ cout<<"BaseB constructor"<<endl; } BaseB::~BaseB(){ cout<<"BaseB destructor"<<endl; } //派生类 class Derived: public BaseA, public BaseB{ public: Derived(int a, int b, int c, int d, int e); ~Derived(); public: void show(); private: int m_e; }; Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e){ cout<<"Derived constructor"<<endl; } Derived::~Derived(){ cout<<"Derived destructor"<<endl; } void Derived::show(){ cout<<m_a<<", "<<m_b<<", "<<m_c<<", "<<m_d<<", "<<m_e<<endl; } int main(){ Derived obj(1, 2, 3, 4, 5); obj.show(); return 0; }
运行结果:
BaseA constructor BaseB constructor Derived constructor 1, 2, 3, 4, 5 Derived destructor BaseB destructor BaseA destructor
从运行结果中还可以发现,多继承形式下析构函数的执行顺序和构造函数的执行顺序相反。
多继承下的二义性问题
什么是多重继承的二义性
class A{ public: void f(); } class B{ public: void f(); void g(); } class C:public A,public B{ public: void g(); void h(); };
如果声明:C c1,则c1.f();具有二义性,而c1.g();无二义性(同名覆盖)。
解决办法1:-- 类名限定
调用时指名调用的是哪个类的函数,如
c1.A::f();
c1.B::f();
解决办法2:-- 同名覆盖
在C中声明一个同名函数,该函数根据需要内部调用A的f或者是B的f。如
class C:public A,public B{ public: void g(); void h(); void f(){ A::f(); } };
还有一种解决方法就是通过虚函数,下面我们将讲解虚函数的用法。
9. 虚函数
尽管派生列表中一个基类只能出现一次,但实际上派生类可以多次继承同一个类,派生类可以通过它的两个直接基类分别继承同一个基类。在这种情况下,派生类将包含该类的多个子对象。
在C++中,我们通过虚继承解决这个问题,虚继承的目的是令某个类做出声明,承诺愿意共享它的基类,其中共享的基类子对象被称为虚基类。不论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。我们通过在派生列表中添加关键字virtual来指定虚基类。
虚基类是由最低层的派生类初始化的,即使虚基类不是派生类的直接基类,派生类的构造函数也可以初始化虚基类,含有虚基类的对象在构造时,首先会初始化虚基类部分,接下来才按照直接基类在派生列表中出现的次序进行初始化。
class ClassA { public: ClassA(int height):height_(height) { std::cout<<"constructor A"<<std::endl; }; void funcA() { std::cout << "in funcA: height_ = "<< height_ << std::endl; } protected: int height_; }; class ClassB:public virtual ClassA { public: ClassB():ClassA(10) { std::cout<<"constructor B"<<std::endl; } void funcB() { std::cout << "in funcB: height_ = "<< height_ << std::endl; } }; class ClassC :public virtual ClassA { public: ClassC() :ClassA(100) { std::cout<<"constructor C"<<std::endl; } void funcC() { std::cout << "in funcC: height_ = "<< height_ << std::endl; } }; class Worker :public ClassB, public ClassC { public: Worker():ClassB(),ClassC(),ClassA(50) { std::cout<< "constructor Worker" <<std::endl; } void funcWorker(){ std::cout << "in funcWorker: height_ = " << height_ << std::endl; } }; int main() { Worker w; std::cout << "------------" <<std::endl; w.funcA(); w.funcB(); w.funcC(); w.funcWorker(); int i; std::cin >> i; return 0; }
结果:
constructor A constructor B constructor C constructor Worker ------------ in funcA: height_ = 50 in funcB: height_ = 50 in funcC: height_ = 50 in funcWorker: height_ = 50
从运算结果看出,类A的实例只初始化了一次,无论Worker的构造函数的书写顺序是怎样的,只要有虚基类(就是本案例中的类A),那么虚基类的实例将会被首先构造。当后面的类试图再次构造虚基类的实例时就会被拒接(类B和类C都没有再次构造类A的实列)。本例中在类Worker中必须显式的调用类A的构造函数,若不显式调用类A的构造函数则编译器不知道以谁构造的类A实列为准(类B构造的还是类C构造的?)