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",), *p = &person;
  //也可以写成:Person person = Person("jamy",20);
cout << "name:" << p->getName() << "\n"
<< "age:" << p->getAge() << endl; return ;
}

在定义类的时候,可以使用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 = ;//可以在类内部初始化 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 ;
}

编译以上代码会出现“对‘A::a’未定义的引用”错误。这是因为静态成员变量a未定义,也就是还没有分配内存,显然是不可以访问的。

    #include <stdio.h>
class A {
public:
static int a; //声明但未定义
};
int A::a = ; //定义了静态成员变量,同时初始化。也可以写"int A:a;",即不给初值,同样可以通过编译
int main() {
printf("%d", A::a);
return ;
}

这样就对了,因为给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() = ;
}
//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(, , , , );
obj.show();
return ;
}

运行结果:

BaseA constructor
BaseB constructor
Derived constructor
, , , ,
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()
{
std::cout<<"constructor B"<<std::endl;
}
void funcB()
{
std::cout << "in funcB: height_ = "<< height_ << std::endl;
}
}; class ClassC :public virtual ClassA
{
public:
ClassC() :ClassA()
{
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()
{
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 ;
}

结果:

constructor A
constructor B
constructor C
constructor Worker
------------
in funcA: height_ =
in funcB: height_ =
in funcC: height_ =
in funcWorker: height_ =

从运算结果看出,类A的实例只初始化了一次,无论Worker的构造函数的书写顺序是怎样的,只要有虚基类(就是本案例中的类A),那么虚基类的实例将会被首先构造。当后面的类试图再次构造虚基类的实例时就会被拒接(类B和类C都没有再次构造类A的实列)。本例中在类Worker中必须显式的调用类A的构造函数,若不显式调用类A的构造函数则编译器不知道以谁构造的类A实列为准(类B构造的还是类C构造的?)

【C++】C++中类的基本使用的更多相关文章

  1. PHP中类自动加载的方式

    最近在学习composer,发现从接触PHP到现在已经遇到了三种关于PHP中类的自动加载方式,这其中包括PHP自带的类的自动加载方式.PHP的第三方的依赖管理工具composer的加载方式以及PHP的 ...

  2. Typescript 中类的继承

    Typescript中类的定义与继承与后端开发语言java/C#等非常像,实现起来非常方便,而且代码便于阅读. 用Typescript写较大项目时是非常有优势的. /** * BaseClass */ ...

  3. Objective-C 中类属性(修饰)

    Objective-C 中类属性(修饰) (2013-07-13 14:38:35) 转载▼ 标签: it 分类: IOS笔记 nonatomic: 非原子性访问,对属性赋值的时候不加锁,多线程并发访 ...

  4. java中类的创建及初始化过程

    java中类的创建及初始化过程无外乎两种情况,其一为单类的创建及初始化,其二具有继承关系的父子类创建及初始化过程.     首先说简单的,单类的创建及初始化过程.在java中我们都知道绝大部分对象的创 ...

  5. Python中类的特殊方法详解

    本文和大家分享的主要是python语言中类的特殊方法相关用法,希望对大家有帮助. 构造序列 1._len_(self) 2._getitem_(self,key) 3._setitem_(self,k ...

  6. OC基础--OC中类的定义

    OC中类的定义与使用跟C#和java差别相当明显,做个笔记,牢记并加以区别! 一.OC中类的定义:关键字@implementation 和 @end 注意事项: 1.定义好一个类之后,要让这个类继承N ...

  7. UML中类之间的几种关系

    类之间可能存在以下几种关系:关联(association).依赖(dependency).聚合(Aggregation,也有的称聚集).组合(Composition).泛化(generalizatio ...

  8. Java中类的加载、连接和初始化

    Java中类的加载.连接和初始化 类的加载.连接和初始化 先介绍一下JVM和类 JVM和类: 当我们调用Java命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多 ...

  9. C++ 中类的构造函数理解(二)

    C++ 中类的构造函数理解(二) 写在前面 上次的笔记中简要的探索了一下C++中类的构造函数的一些特性,这篇笔记将做进一步的探索.主要是复制构造函数的使用. 复制构造函数 复制构造函数也称拷贝构造函数 ...

  10. IOS中类和对象还有,nil/Nil/NULL的区别

    转自:http://blog.sina.com.cn/s/blog_5fb39f910101akm1.html 类与对象的概念 类是对同一类事物高度的抽象,类中定义了这一类对象所应具有的静态属性(属性 ...

随机推荐

  1. springcloud(六):配置中心git示例

    随着线上项目变的日益庞大,每个项目都散落着各种配置文件,如果采用分布式的开发模式,需要的配置文件随着服务增加而不断增多.某一个基础服务信息变更,都会引起一系列的更新和重启,运维苦不堪言也容易出错.配置 ...

  2. spring mvc注解版01

    spring mvc是基于servlet实现的在spring mvc xml版中已经说过了,注解版相较于xml版更加简洁灵活. web项目的jar包: commons-logging-1.1.3.ja ...

  3. 实验吧debug

    在linux下进行调试时容易出现权限不够的情况:此时解决办法就是chmod 777+文件名提升权限,以实验吧debug为例,给出了简单的32elf文件,我在查看一些资料以后发现,我需要在main函数处 ...

  4. pygm2安装问题

    pygm2是python的一个库,它提供了大部分数学处理的方式,今天在查看自己环境后,发现这个环境还没有安装上,于是,自己动手丰衣足食吧,我的系统为win10家庭版,首先执行的pip install ...

  5. asp.net core 依赖注入实现全过程粗略剖析(3)

    接着 前面,前面的过程是普遍常用的依赖注入解析过程,我们正常都是在startup类中注入依赖服务,但是,笔者这周开发的时候遇到个问题,不同服务的生命周期不同,不能调用服务.举个例子,AddDbCont ...

  6. 有关python 函数参数

    # def foo(x):# print(x)### foo(1)# foo('a')# foo({'a':2}) #形参与实参:# def foo(x,y): #x=1,y=2# return x+ ...

  7. SpringBoot整合elasticsearch

    在这一篇文章开始之前,你需要先安装一个ElasticSearch,如果你是mac或者linux可以参考https://www.jianshu.com/p/e47b451375ea,如果是windows ...

  8. Cisco 12系列 AP 初始化配置-1-安装IOS

    12系列AP虽然已经淘汰了,但是像我们这种没钱的公司用了10年却还是在用,好在它还有学习的价值,还是可以从12系列AP看出一些思科部署无线的思路吧. 首先吐槽一下国内常说的胖.瘦AP的这种说法,因为用 ...

  9. bootstrap-year-calendar全年日历插件

    使用方法使用bootstrap-year-calendar插件需要引入jQuery.Bootstap3的相关依赖文件和插件本身需要的js和css文件. <link rel="style ...

  10. SpringMVC url匹配却404,SimpleUrlHandlerMapping不起作用

    代码如下: <mvc:default-servlet-handler/> <bean class="org.springframework.web.servlet.hand ...