从零开始学C++之类与对象:类声明、类作用域、前向声明、this指针、嵌套类、PIMPL 技法 等

一、类声明

//类是一种用户自定义类型,声明形式: class 类名称 {    public:              公有成员(外部接口)    private:              私有成员    protected:              保护成员 };

在关键字public后面声明,它们是类与外部的接口,任何外部函数都可以访问公有类型数据和函数。 在关键字private后面声明,只允许本类中的函数访问,而类外部的任何函数都不能访问。 在关键字protected后面声明,与private类似,其差别表现在继承与派生时对派生类的影响不同。

class Clock
{
public:
    void Display();
    void Init(int hour, int minute, int second);

private:
    int hour_;
    int minute_;
    int second_;
};

假设定义了一个Clock 类,因为成员是private的,那么 Clock ck;  ck.hour_ = 12; 是错误的,对此定义一个public 的void SetHour(int hour) 来设置hour_ 的值。

二、内联成员函数、成员函数的重载及其缺省参数

这里有内联函数的概念。成员函数也可以是内联的,若在类内部实现,inline 关键字可加可不加;在类外部实现,需加inline,

如 inline void Clock::SetHour(int hour) { } 。实际上即使加了inline也不一定宏展开,比如遇到switch,for 语句的时候就往往不会。

此外,成员函数也像一般函数那样可以重载,也可以有缺省参数,参考这里

三、类与结构体

class与struct的区别:在未指定访问权限时,class默认的是私有的,struct默认是公有的,

struct Test {     int X;//公有的      ... };

此外,Test 可以独立作为一个tag,而不像C语言那样需要 struct Test 作为一个类型。

四、隐含的 this 指针

成员函数有一个隐含的附加形参,即指向该对象的指针,这个隐含的形参叫做this指针(编译器自动传递)

使用this指针保证了每个对象可以拥有不同数值的数据成员,但处理这些成员的代码可以被所有对象共享

成员函数是只读的代码,由所有对象共享,并不占对象的存储空间,因为this指针指向当前对象,所以成员函数可以区分它所作用的对象是哪一个。

(哪个对象调用了this所在的函数,this就代表哪个对象)

?

再来看一道经典的题目:

class A
{
public:
    int m;
    void print()
    {
        cout << "A" << endl;
    }
};

A *pa = 0;
pa->print();

可以理解为如下的C代码:

void print(A *this)
{
    cout << "A" << endl;
}
A *pa = 0;
print_A(pa);

相当于成员函数传递的this指针为0,那调用会出错吗? 肯定是正确输出"A" 的,因为this为0 表示没有对某个对象进行操作,而print里面确实没有对某个对象成员进行操作,所以是可以运行的。

五、类作用域、前向声明

(1)、每个类都定义了自己的作用域称为类作用域,类作用域中说明的标识符只在类中可见。除了类作用域,还有块作用域、文件作用域、函数原型作用域、函数作用域,举个例子:

#include <iostream>
using namespace std;

class Test
{
public:
    int num_;
};

//num_ = 20;        Error,num_的作用域在类内部
int num_ = 20;      // num_的作用域是文件作用域,与类中的num_是不同的作用域

int add(int a, int b);  // a, b两个标识符的作用域为函数原型作用域

int main(void)
{
    int num_ = 30;      // num_为块作域
    {
        int num_ = 100; // num_为块作域
    }

    cout << num_ << endl;
    cout <<::num_ << endl;
    return 0;
}

int add(int a, int b)   // 形参a与b也算是块作用域
{
    return a + b;
}

int test()
{
LABEL1: //函数作用域
    cout << "label1" << endl;
    goto LABEL3;
LABEL2:
    cout << "label2" << endl;
    goto LABEL1;
LABEL3:
    cout << "label3" << endl;
    goto LABEL2;
}

(2)、C++中类必须先定义,才能够实例化。两个类需要相互引用头文件形成一个“环形”引用时会出错。这时候需要用到前向声明,前向声明的类不能实例,但可以定义指针或引用。

#ifndef _B_H_
#define _B_H_

class A;

class B
{
public:
    B(void);
    ~B(void);

    void fun(A &a)
    {

    }

    A *a_;      // 前向声明的类不能实例化对象
};

#endif // _B_H_
#ifndef _A_H_
#define _A_H_

#include "B.h"
class A
{
public:
    A(void);
    ~A(void);

    B b_;
};

#endif // _A_H_

六、嵌套类、局部类

(1)、嵌套类

外围类需要使用嵌套类对象作为底层实现,并且该嵌套类只用于外围类的实现,且同时可以对用户隐藏该底层实现。 从作用域的角度看,嵌套类被隐藏在外围类之中,该类名只能在外围类中使用。如果在外围类之外的作用域使用该类名时,需要加名字限定。 嵌套类中的成员函数可以在它的类体外定义。 嵌套类的成员函数对外围类的私有成员没有访问权,反之亦然。 嵌套类仅仅只是语法上的嵌入

(2)、局部类

类也可以定义在函数体内,这样的类被称为局部类(local class)。局部类只在定义它的局部域内可见。 局部类的成员函数必须被定义在类体中。 局部类中不能有静态成员,关于类中的静态成员和静态成员函数以后再谈。

#include <iostream>
using namespace std;

class Outer
{
public:
    class Inner
    {
    public:
        void Fun();
        //{
        //  cout<<"Inner::Fun ..."<<endl;
        //}
    };
public:
    Inner obj_;
    void Fun()
    {
        cout << "Outer::Fun ..." << endl;
        obj_.Fun();
    }
};

void Outer::Inner::Fun()
{
    cout << "Inner::Fun ..." << endl;
}

void Fun()
{
    class LocalClass
    {
    public:
        int num_;
        void Init(int num)
        {
            num_ = num;
        }
        void Display()
        {
            cout << "num=" << num_ << endl;
        }

        //static int num2_; // 局部类内部不能定义静态成员
    };

    LocalClass lc;
    lc.Init(10);
    lc.Display();
}

int main(void)
{
    Outer o;
    o.Fun();

    Outer::Inner i;
    i.Fun();

    Fun();
    //LocalClass lc;        Error,局部类只能在定义它的函数体中使用
    return 0;
}

七、PIMPL 技法

来看下面的示例:

// file y.h
#include "x.h"
class Y
{
    void Fun();
    X x_;

};
// file y.cpp
#include "y.h"
void Y::Fun
{
    return x_.Fun();
}
// file main.cpp
#include “y.h”
int main(void)
{
    Y y;
    y.Fun();
}

上面程序存在的问题是:

1、引入更多的头文件,降低编译速度

2、在编译期如果X的大小改变了,y.cpp 和 main.cpp 都得重新编译;在运行期,如果X有子类,也不能使用多态虚函数。

3、假设y.cpp 编译成动态库给main.cpp 使用,当X的大小变化,动态库需要重新编译,此时main.cpp 因为有定义对象y ,故也需要

重新编译。

下面介绍一种PIMPL 技法,有人也把它当作一种设计模式:

PIMPL(private implementation或pointer to implementation)也称为handle/body idiom

PIMPL背后的思想是把客户与所有关于类的私有部分的知识隔离开。避免其它类知道其内部结构

降低编译依赖、提高重编译速度 接口和实现分离 降低模块的耦合度

编译期 运行期

提高了接口的稳定程度

对于库的使用,方法不用改变 对于库的编译,动态库的变更,客户程序不用重新编译

修改后的程序:

// file y.h
class X;
class Y
{
    Y();
    ~Y();
    void Fun();
    X *px_;
};
// file y.cpp
#include "x.h"
Y::Y() : px_( new X ) {}
Y::~Y()
{
    delete px_;
    px_ = 0;
}
void Y::Fun()
{
    return px_->Fun();
}
//  file main.cpp
#include "y.h"
int main(void)
{
    Y y;
    y.Fun();
}

即Y 内部成员是X* 指针,在32位系统上,指针大小固定为4个字节,即使X大小改变,也不影响Y。如果X 有子类,通过基类指针px_ 

还可以实现虚函数多态。

参考:

C++ primer 第四版 Effective C++ 3rd C++编程规范

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏緣來來來

Python中lambda的学习

这个函数引用时需要传入一个参数,并且有一个返回值。这个参数一般是for x in L之类的传进来,或者直接调用f(3)。

532
来自专栏Ryan Miao

javascript中遇到的字符串对象处理

在javascript中对参数处理: 1 <script> 2 function getParam() 3 { 4 urlInfo=window.loc...

33611
来自专栏和蔼的张星的图像处理专栏

365. 二进制中有多少个1

思路一:遍历每一位,如果是1,计数器加1即可,也是最容易想到的,需要遍历一次,可以用不断除2来做,也可以用位操作,后者更简单些:

582
来自专栏杂七杂八

简单的正则表达式

特殊字符 ^ $ * ? + {2} {2,} {2,5} | [] [^] [a-z] . \s \S \w \W [\u4E00-\u9FA5] ...

2646
来自专栏Golang语言社区

Go语言基本语法

前面已经看到了Go程序的基本结构,所以这将是很容易理解Go编程语言等基本构建块。 Go令牌 Go程序包括各种令牌和令牌可以是一个关键字,一个标识符,常量,字符串...

2586
来自专栏Hongten

python开发_函数的参数传递

在这个用例中,我们要讨论的是关于函数的传参问题 我所使用的python版本为3.3.2

714
来自专栏web

js中push(),pop(),unshift(),shift()的用法小结

872
来自专栏西枫里博客

Python学习笔记七(序列操作)

本次学习内容是首先是回顾下昨天的format格式化函数。接着上一次格式化字符串操作的后续课程,序列的操作。什么是序列?序列是有列表、元组、字符串构成,它们之间有...

782
来自专栏闵开慧

$.each()与$(selector).each()区别详解

$.each()与$(selector).each()不同, 后者专用于jquery对象的遍历, 前者可用于遍历任何的集合(无论是数组或对象),如果是数组,回...

35212
来自专栏深度学习之tensorflow实战篇

mongodb11天之屠龙宝刀(九)js函数入门:MongoDB基于js的数据类型修改

mongodb11天之屠龙宝刀(九)js函数入门:MongoDB基于js的数据类型修改 Mongodb并不提供Alter table这样的语句或者工具修...

2924

扫码关注云+社区