0%

1. 连接操作符:##

#define Conn(x,y) x##y

## 表示连接 , x##y 表示x连接y

示例:

int n = Conn(123,456);
     ==> int n=123456;
char* str = Conn("asdf", "adf");
     ==> char* str = "asdfadf";

## 的左右符号必须能够组成一个有意义的符号,否则预处理器会报错

2.字符串化和字符化: #, #@

(1) # 把任意类型的宏入参转化成字符串:

#define ToString(x) #x

符号 # 表示字符串化操作符(stringification)。
其作用是:将宏定义中的传入参数名转换成用一对双引号括起来参数名字符串。
其只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前。

示例:

 char* str = ToString(123132);
 ==> char* str="123132";

如果要对展开后的宏参数进行字符串化,则需要使用两层宏。

#define xstr(s) str(s)
#define str(s) #s
#define foo 4

str (foo)
     ==> "foo"
xstr (foo)
     ==> xstr (4)
     ==> str (4)
     ==> "4"

(2) #@ 把任意类型的宏入参转化成单字符:

#define ToChar(x) #@x

示例:

char a = ToChar(1);
     ==> char a='1'

3. 不定参数宏: __VA_ARGS__

__VA_ARGS__宏用来接受不定数量的参数。例如:

#define eprintf(...) fprintf (stderr, __VA_ARGS__)

eprintf ("%s:%d: ", input_file, lineno)
==>  fprintf (stderr, "%s:%d: ", input_file, lineno)

__VA_ARGS__宏前面加 ## 时,可以省略参数输入。
例如:

#define eprintf(format, ...) fprintf (stderr, format, ##__VA_ARGS__)

eprintf ("success!\n")
==> fprintf(stderr, "success!\n");

4. 宏函数定义: do-while(0)与换行

(1) 用 do{}while(0) 定义宏函数

#define foo() do{...}while(0)

宏函数可能在任意地方被调用,如果在if-else语句中,为了确保宏函数作为整体单元被编译器替换后不产生歧义,一般用do-while(0)包起来定义
这种定义有点像其他语言的“闭包函数”,纯粹是C语言中为了避免语法歧义的可靠性定义。

(2) 用显式换行符

宏函数定义不能直接回车换行,需要在回车换行前,用\(反斜线)表示下一行继续此宏的定义
预处理器在编译之前会自动将\与换行回车去掉。

例如:

#define PRINT_INT(a)    \
do{                     \
    printf("%d \n", a); \
}while(0)

1. 函数指针基本概念

C语言调用函数的本质是什么?

  1. CPU PC指针(program counter)跳转到函数的入口地址,传入参数,传入返回位置
  2. 在函数栈内生成临时变量并执行函数内容指令,执行完毕后返回参数
  3. CPU返回原调用处执行

这里,函数的入口地址实际上就存储在函数名中,也就是说,C代码中的函数名就是函数的入口地址。
既然是地址,就可以用来初始化一个指针,使指针指向该地址。
函数指针,就是存放函数首地址的指针。

1.2 函数指针变量

首先声明普通函数是如下格式:
void Func(int);
定义一个同类型函数的函数指针变量,只需要用*p表示函数名即可:
void (*p)(int);
注意,上面是定义了函数指针变量,而不是声明函数指针类型。

函数指针变量的定义,和普通变量格式不一样。

  • 普通变量: <类型> <变量名>
  • 函数指针:<函数类型 变量名>,按函数声明的格式定义,变量是包含在类型内部

那么此函数指针的类型是什么:
void (*)(int);

怎么使用此函数指针:

1
2
3
4
5
6
7
void Func(int x) // 声明一个函数*/
{
printf("%d",x);
}
void (*p)(int); // 定义一个函数指针*/
p = Func; // 将Func函数的首地址赋给函数指针变量p*/
(*p)(100); // 通过函数指针调用Func函数

1.3 函数指针类型

typedef可以定义某种类型的别名,例如将unsigned char定义为u8
typedef unsigned char u8;
可见其格式是:typedef <原类型> <别名类型>

那么如何定义函数指针类型:
只需要在函数指针声明的格式前加typedef, p就定义为函数指针类型而不是变量:
typedef void (*p)(int);

这里定义了void (*)(int)类型的函数指针类型,其别名为p

怎么使用此函数指针类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义类型
typedef void (*pFuncType)(int);
//定义变量
pFuncType p;

void Func(int x)
{
printf("%d",x);
}

void main()
{
p = Func; //初始化变量
(*p)(100); //使用变量
}

2. 函数指针的应用

2.1 Linux驱动软件设计的分层

C++中有多态的概念,父类定义某种函数类型,子类实现具体的函数内容,之后父类对象就可以调用子类的实现内容。
这样实现“父类定义格式,子类实现细节”的软件分层设计。

Linux驱动中也大量使用这种分层设计,C使用结构体和函数指针,分别对应C++的类和方法。
例如某驱动模块,顶层框架定义open, read, write, 而底层驱动根据具体硬件,实现xxx_open, xxx_read, xxx_write,然后按结构体成员赋值实现上下层联通:

以s3c的SDHCI驱动为例:
sdhci_ops即父类框架结构体,sdhci_s3c_ops为s3c芯片的具体实现结构体。
.set_clock = sdhci_s3c_set_clock就实现了父类的set_clock方法被子类sdhci_s3c_set_clock实现的效果。
其本质是函数指针的赋值,父类和子类的函数名都指向同一个函数地址,所以父类可以通过调用set_clock来实现调用sdhci_s3c_set_clock的效果。
这样,父类设计者根本不关心底层用什么函数名实现set_clock的功能,只需要调用set_clock这个函数名即可。

1
2
3
4
5
6
7
8
static struct sdhci_ops sdhci_s3c_ops = {
.get_max_clock = sdhci_s3c_get_max_clk,
.set_clock = sdhci_s3c_set_clock,
.get_min_clock = sdhci_s3c_get_min_clock,
.set_bus_width = sdhci_set_bus_width,
.reset = sdhci_reset,
.set_uhs_signaling = sdhci_set_uhs_signaling,
};

2.2 函数指针实现指令跳转

调用一个函数,其内部就包含跳转操作(jump指令)
那么,如何用C语言实现跳转到某个绝对地址去执行?具体场景:
在Bootloader过程中,加载完kernel到RAM后,要跳转到kernel首地址开始执行,如何实现?

方案一:C嵌入汇编
以下是基于SPARC CPU的汇编,跳转到0x40000000, 即RAM首地址执行firmware.
对于其他CPU,汇编实现也不同,因此此方法不能跨平台。

1
2
3
4
5
6
7
8
9
10
11
void boot_exit()
{
/* jump to RAM entry to execute firmware. */

asm(
"set 0x40000000, %g2\n"
"jmp %g2\n"
"nop"
);

}

方案二:函数指针
Bootloader中很常用的一种跳转方法

1
2
3
typedef void (*pFuncType)(); //定义无入参无返回类型的函数指针类型
pFuncType Reset = (pFuncType)0xF000FFF0; //函数指针指向待跳转地址
Reset(); //调用函数,实际上执行了跳转

0.概述

C++除了支持面向对象,也支持函数的扩展和重用。继承类和多态充分支持了扩展,即基类进行概括抽象的设计,派生类实现具体的设计。那么重用呢,如何写一个函数,能在多种情况不加修改的套用?
考虑以下问题:

交换两个整型变量的值的Swap函数:
void Swap(int & x,int & y)
{
    int tmp = x;
    x = y;
    y = tmp;
}
交换两个double型变量的值的Swap函数:
void Swap(double & x,double & y)
{
    double tmp = x;
    x = y;
    y = tmp;
}

这两个函数除了入参类型不同,函数名,参数个数,返回类型都相同。函数体的处理流程也完全一样。能否只写一个Swap,就能交换各种类型的变量?
模板(template)将解决这种问题。

函数模板

函数模板的概念

用函数模板,设计仅数据类型不同的一组函数的通用模板:

template <class 类型参数1,class 类型参数2,……>
返回值类型 模板名 (形参表)
{
    函数体
};

template <class T> //在函数前声明模板,参数类型(class)是T
void Swap(T & x,T & y)
{
    T tmp = x;
    x = y;
    y = tmp;
}

在普通函数前,先用template< class T >声明参数类型T,就可以在函数体使用T,就像使用普通变量一样。在运行时,T传入的类型不同,函数处理的数据类型也不同。
函数模板是如何实现的?它是一种函数吗?

int main()
{
    int n = 1,m = 2;
    Swap(n,m); //编译器自动生成 void Swap(int & ,int & )函数
    double f = 1.2,g = 2.3;
    Swap(f,g); //编译器自动生成 void Swap(double & ,double & )函数
    return 0;
}

函数模板只是个模子,编译器通过模板和具体参数类型产生不同的函数。编译器会进行两次编译,第一次检测模板代码,第二次检测加参数后的具体函数代码。
在调用以上函数模板时,实际会生成两个具体函数:

void Swap(int & x,int & y)
{
    int tmp = x;
    x = y;
    y = tmp;
}
void Swap(double & x,double & y)
{
    double tmp = x;
    x = y;
    y = tmp;
}

函数模板,可以理解为一种函数的宏,编译期在宏被调用的地方,全部替换成宏所对应的具体值。
函数模板和运行时函数,也类似于类和对象的关系,类只是类型,对象是给类分配了内存的实例。函数模板只是通用的类型,函数实例是函数模板的实例化对象。

函数模板的特性

函数模板中可以有不止一个类型参数

template <class T1, class T2>
T2 print(T1 arg1, T2 arg2)
{
    cout<< arg1 << " "<< arg2<<endl;
    return arg2;
}

不通过参数也能实例化函数模板

template <class T>
T Inc(T n)
{
    return 1 + n;
}
int main()
{
    cout << Inc<double>(4)/2; //显式实例化模板,输出 2.5
    return 0;
}

函数模板与重载

函数模板和函数重载,在写法上很接近,都是一组参数不同的同名函数。它们有什么联系和区别?

  • 函数重载,关键在参数个数
  • 函数模板,关键在参数类型

函数模板可以重载,只要它们的形参表或类型参数表不同即可

template<class T1, class T2>
void print(T1 arg1, T2 arg2) {
    cout<< arg1 << " "<< arg2<<endl;
}
template<class T>
void print(T arg1, T arg2) {
    cout<< arg1 << " "<< arg2<<endl;
}
template<class T,class T2>
void print(T arg1, T arg2) {
    cout<< arg1 << " "<< arg2<<endl;
}

在有多个函数和函数模板名字相同的情况下,编译器如下处理一条函数调用语句:

  1. 先找参数完全匹配的普通函数(非由模板实例化而得的函数)

  2. 再找参数完全匹配的模板函数。

  3. 再找实参数经过自动类型转换后能够匹配的普通函数。

  1. 上面的都找不到,则报错
    如果有函数重载,在第(1)步就找到,如果没有,才找(2)的函数模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    template <class T>
    T Max( T a, T b) {
    cout << "TemplateMax" <<endl; return 0;
    }
    template <class T,class T2>
    T Max( T a, T2 b) {
    cout << "TemplateMax2" <<endl; return 0;
    }
    double Max(double a, double b){
    cout << "MyMax" << endl;
    return 0;
    }

    int main() {
    int i=4, j=5;
    Max( 1.2,3.4); // 输出MyMax
    Max(i, j); //输出TemplateMax
    Max( 1.2, 3); //输出TemplateMax2
    return 0;
    }

注意上文的步骤(2)(3)是分开的,匹配模板函数时,是不进行类型自动转换的,不匹配就是不匹配。输入参数有二义性时。编译器不会为开发者做决定

template<class T>
T myFunction( T arg1, T arg2)
{ cout<<arg1<<" "<<arg2<<"\n"; return arg1;}
……
myFunction( 5, 7); //ok: replace T with int
myFunction( 5.8, 8.4); //ok: replace T with double
myFunction( 5, 8.4); //error, no matching function for call to 'myFunction(int, double)'

类模板

类模板的概念

类也能使用模板,来生成不同成员类型的类
类模板:在定义类的时候,加上一个/多个类型参数。在使用类模板时,指定类型参数应该如何替换成具体类型,编译器据此生成相应的模板类

template <class 类型参数1, class 类型参数2, ……> //类型参数表
class 类模板名
{
    成员函数和成员变量
};

类模板的成员函数的定义写法:

template <class 类型参数1, class 类型参数2, ……> //类型参数表
返回值类型 类模板名<类型参数名列表>::成员函数名( 参数表)
{
    ……
}

用类模板实例化对象的写法:

类模板名 <真实类型参数表> 对象名(构造函数实参表);

一个例子:map类型中的pair类的实现:

template <class T1,class T2>    //pair是类模板
class Pair
{
public:
    T1 key; //关键字
    T2 value; //值
    Pair(T1 k,T2 v):key(k),value(v) { }; //构造函数
    bool operator < ( const Pair<T1,T2> & p) const; //运算符重载函数
};

template<class T1,class T2>
bool Pair<T1,T2>::operator < ( const Pair<T1,T2> & p) const
//Pair的运算符重载函数的定义
{
    return key < p.key;
}

 int main()
{
    Pair<string,int> student("Tom",19); //实例化出一个类 Pair<string,int>
    cout << student.key << " " << student.value;
    return 0;
}

输出:Tom 19
编译器由类模板生成类的过程叫类模板的实例化。 由类模板实例化得到的类, 叫模板类
同一个类模板的两个模板类是不兼容的,即两个不同的类

Pair<string,int> * p;
Pair<string,double> a;
p = & a; //错误,不是同类也不是继承类,不能赋值

函数模版可以作为类模板成员

template <class T>
class A
{
public:
    template<class T2>
    void Func( T2 t) { cout << t; } //成员函数模板
};
int main()
{
    A<int> a;
    a.Func('K'); //成员函数模板 Func被实例化
    a.Func("hello"); //成员函数模板 Func再次被实例化
    return 0;
} //输出: KHello

类模板的“<类型参数表>”中可以出现非类型参数:

template <class T, int size>
class CArray{
    T array[size];
    public:
    void Print( )
    {
        for( int i = 0;i < size; ++i)
        cout << array[i] << endl;
    }
};

CArray<double,40> a2;
CArray<int,50> a3;

类模板的派生

类模板也支持类的派生:
• 类模板从类模板派生
• 类模板从模板类派生
• 类模板从普通类派生
• 普通类从模板类派生

(1)类模板从类模板派生

template <class T1,class T2>
class A {
    T1 v1; T2 v2;
};

template <class T1,class T2>
class B:public A<T2,T1> {
    T1 v3; T2 v4;
};

template <class T>
class C:public B<T,T> {
    T v5;
};

int main() {
    B<int,double> obj1;
    C<int> obj2;
    return 0;
}

(2)类模板从模板类派生

template <class T1,class T2>
class A {
    T1 v1; T2 v2;
};

template <class T>
class B:public A<int,double> {
    T v;
};

int main() {
    B<char> obj1; //自动生成两个模板类:A<int,double> 和 B<char>
    return 0;
}

(3)类模板从普通类派生

class A {
    int v1;
};

template <class T>
class B:public A { //所有从B实例化得到的类, 都以A为基类
    T v;
};

int main() {
    B<char> obj1;
    return 0;
}

(4)普通类从模板类派生

template <class T>
class A {
    T v1;
    int n;
};

class B:public A<int> {
    double v;
};
int main() {
    B obj1;
    return 0;
}

类模板与友元

• 函数、类、类的成员函数作为类模板的友元
• 函数模板作为类模板的友元
• 函数模板作为类的友元
• 类模板作为类模板的友元

(1)函数、类、类的成员函数作为类模板的友元

void Func1() { }
class A { };
class B
{
    public:
    void Func() { }
};

template <class T>
class Tmpl
{
    friend void Func1();
    friend class A;
    friend void B::Func();
}; //任何从Tmp1实例化来的类, 都有以上三个友元

(2)函数模板作为类模板的友元

template <class T1,class T2>
class Pair
{
private:
    T1 key; //关键字
    T2 value; //值
public:
    Pair(T1 k,T2 v):key(k),value(v) { };
    bool operator < ( const Pair<T1,T2> & p) const;
    template <class T3,class T4>
    friend ostream & operator<< ( ostream & o,
    const Pair<T3,T4> & p);
};

template<class T1,class T2>
bool Pair<T1,T2>::operator < ( const Pair<T1,T2> & p) const
{ //"小"的意思就是关键字小
    return key < p.key;
}
template <class T1,class T2>
ostream & operator<< (ostream & o,const Pair<T1,T2> & p)
{
    o << "(" << p.key << "," << p.value << ")" ;
    return o;
}

int main()
{
    Pair<string,int> student("Tom",29);
    Pair<int,double> obj(12,3.14);
    cout << student << " " << obj;
    return 0;
}

输出:
(Tom,29) (12,3.14)

任意从 template <class T1,class T2> ostream & operator<< (ostream & o,const Pair<T1,T2> & p)生成的函数,都是任意Pair摸板类的友元

(3)函数模板作为类的友元

class A
{
    int v;
    public:
    A(int n):v(n) { }
    template <class T>
    friend void Print(const T & p);
};
template <class T>
void Print(const T & p)
{
    cout << p.v;
}

int main() {
    A a(4);
    Print(a);
    return 0;
}

输出:
4

所有从 template <class T> void Print(const T & p)
生成的函数,都成为 A 的友元

(4)类模板作为类模板的友元

template <class T>
class B {
    T v;
    public:
    B(T n):v(n) { }
    template <class T2>
    friend class A;
};

template <class T>
class A {
public:
    void Func( ) {
        B<int> o(10);
        cout << o.v << endl;
    }
};

int main()
{
    A< double > a;
    a.Func ();
    return 0;
}

输出:
10

A< double>类,成了B类的友元。任何从A模版实例化出来的类,都是任何B实例化出来的类的友元

类模板与静态成员

类模板中可以定义静态成员,那么从该类模板实例化得到的所有类,都包含同样的静态成员

template <class T>
class A
{
private:
    static int count;
    public:
    A() { count ++; }
    ~A() { count -- ; };
    A( A & ) { count ++ ; }
    static void PrintCount() { cout << count << endl; }
};

template<> int A<int>::count = 0;
template<> int A<double>::count = 0;
int main()
{
    A<int> ia;
    A<double> da;
    ia.PrintCount();
    da.PrintCount();
    return 0;
}

输出:
1 1

0.概述

前文分析了C++类内成员的关系,本文讨论类和类之间的关系。
考虑用C++对现实世界的交通工具进行描述。

  • 汽车可能包含各种类型,小汽车,公交车,但他们能抽象出四个轮子,烧油这些基本属性
  • 飞机也有各种类型,但也能抽象出机翼,机身等基本属性
  • 轮船…

如果自顶向下设计,如何设计这些对象的类?

  • 提炼这些交通工具的共有属性,如材质,耗油量,价格,设计成一个交通工具基础类;然后设计一些操作方法,比如制造,启动,停止。
  • 分别设计汽车、飞机、轮船等更具体的类的属性,比如轮子、排水量等,注意,他们也包含基础类的材质,耗油量,价格等基本属性;然后也设计一些方法,比如制造汽车、开汽车和造飞机、开飞机等
  • 然后再设计更细节的类,作为汽车、飞机、轮船类的细化,比如A品牌的汽车,B品牌汽车,作为两个具体类。

仔细考虑以上步骤,有以下问题:

  • 这些类的属性(成员变量)是相互独立的吗?
  • 这些类的方法(成员函数)是相互独立的吗?

C++用类的“继承”描述层层细化的类及其成员变量的关系,用“多态”描述各层方法的实现关系。

类的继承

继承关系的概念

继承:在定义一个新的类B时,如果该类与某个已有的类A相似(指的是B拥有A的全部特点),那么就可以把A作为一个基类(也叫父类),而把B作为基类的一个派生类(也叫子类)。

  • 派生类是通过对基类进行修改和扩充得到的。在派生类中,可以扩充新的成员变量和成员函数
  • 派生类一经定义后,可以独立使用,不依赖于基类。
  • 派生类拥有基类的全部成员函数和成员变量,不论是private、 protected、 public。但是派生类的成员函数不能访问基类中的private成员

一个管理学生的类继承:
image-20221208165130260
派生类语法:

class 派生类名: public 基类名
{
};

学生类的派生:

class CStudent {
    private:
    string sName;
    int nAge;
    public:
    bool IsThreeGood() { };
    void SetName( const string & name )
    { sName = name; }
        //......
};

class CUndergraduateStudent: public CStudent {
    private:
    int nDepartment;
    public:
    bool IsThreeGood() { ...... }; //覆盖
    bool CanBaoYan() { .... };
}; // 派生类的写法是:类名: public 基类名

类继承的存储空间

在类与对象一文讲过,类对象的存储空间,实际就是成员变量的空间,成员函数不在对象空间内(虚函数包含一个虚函数表指针)。那么基类和派生类的对象空间有什么相关性?
派生类对象的体积,等于基类对象的体积,再加上派生类对象自己的成员变量的体积。 在派生类对象中,包含着基类对象,而且基类对象的存储位置位于派生类对象新增的成员变量之前。
一个示例:

class CBase
{
    int v1, v2;
};
class CDerived:public CBase
{
    int v3;
};

image-20221208165140659

类继承的覆盖

类内的同名非同参的函数叫函数重载,那么基类与派生类的同名函数呢?
派生类可以定义和基类成员同名的成员,这叫覆盖。在派生类中访问这类成员时,默认访问派生类中定义的成员,基类的成员函数或变量被“覆盖”掉了。如果要在派生类中访问基类定义的同名成员时,要使用作用域符号::
一个例子:

class base {    //基类
    int j;  //默认private
    public:
    int i;
    void func();
};
class derived : public base{    //派生类
    public:
    int i;  //覆盖基类i
    void access();
    void func(); //覆盖基类func()
};

void derived::access() { //访问派生类成员
    j = 5; //error
    i = 5; //引用的是派生类的 i
    base::i = 5; //引用的是基类的 i
    func(); //派生类的
    base::func(); //基类的
}

调用函数:

derived obj;
obj.i = 1;  //访问派生类成员i
obj.base::i = 1; //访问基类成员i

内存分布:
image-20221208165150851
以上只是示例,一般来说,基类和派生类不定义同名成员变量,但经常有同名成员函数,所以覆盖通常用于成员函数覆盖。

类继承的成员访问控制

  • 基类的private成员:可以被下列函数访问
    – 基类的成员函数
    – 基类的友元函数
  • 基类的public成员:可以被下列函数访问
    – 基类的成员函数
    – 基类的友元函数
    – 派生类的成员函数
    – 派生类的友元函数
    – 其他的函数
  • 基类的protected成员:可以被下列函数访问
    – 基类的成员函数
    – 基类的友元函数
    – 派生类的成员函数可以访问当前对象的基类的保护成员

一个示例:

class Father {
    private: int nPrivate; //私有成员
    public: int nPublic; //公有成员
    protected: int nProtected; // 保护成员
};
class Son :public Father{
    void AccessFather () {
        nPublic = 1; // ok;
        nPrivate = 1; // wrong
        nProtected = 1; // OK,访问从基类继承的protected成员
        Son f;
        f.nProtected = 1; //wrong , f不是当前对象
    }
};

int main()
{
    Father f;
    Son s;
    f.nPublic = 1; // Ok
    s.nPublic = 1; // Ok
    f.nProtected = 1; // error
    f.nPrivate = 1; // error
    s.nProtected = 1; //error
    s.nPrivate = 1; // error
    return 0;
}

类继承的构造函数

类似于嵌套类(封闭类)的构造函数,使用初始化列表来实现层层构造,基类和派生类只初始化他们能访问的成员

class Bug {
private :
    int nLegs; int nColor;
    public:
    int nType;
    Bug ( int legs, int color);
    void PrintBug (){ };
};

class FlyBug: public Bug // FlyBug是Bug的派生类
{
    int nWings;
    public:
    FlyBug( int legs,int color, int wings);
};

Bug::Bug( int legs, int color) //Bug类的构造函数
{
    nLegs = legs;
    nColor = color;
}

//错误的FlyBug构造函数!
FlyBug::FlyBug ( int legs,int color, int wings)
{
    nLegs = legs; // 不能访问
    nColor = color; // 不能访问
    nType = 1; // ok
    nWings = wings;
}

//正确的FlyBug构造函数:使用初始化列表
FlyBug::FlyBug ( int legs, int color, int wings):Bug( legs, color)
{
    nWings = wings;
}

int main() {
    FlyBug fb ( 2,3,4);
    fb.PrintBug();
    fb.nType = 1;
    fb.nLegs = 2 ; // error. nLegs is private
    return 0;
}

类继承的构造析构时序

在创建派生类的对象时,需要调用基类的构造函数:初始化派生类对象中从基类继承的成员。在执行一个派生类的构造函数之前,总是先执行基类的构造函数。
调用基类构造函数的两种方式:

  • 显式方式:在派生类的构造函数中,为基类的构造函数提供参数.

    derived::derived(arg_derived-list):base(arg_base-list)

  • 隐式方式:在派生类的构造函数中,省略基类构造函数时,派生类的构造函数则自动调用基类的默认构造函数

析构函数执行时序:
派生类的析构函数被执行时,执行完派生类的析构函数后,自动调用基类的析构函数。
一个例子:

class Base {
    public:
    int n;
    Base(int i):n(i)
    { cout << "Base " << n << " constructed" << endl;}
    ~Base()
    { cout << "Base " << n << " destructed" << endl; }
};
    
class Derived:public Base {
    public:
    Derived(int i):Base(i)
    { cout << "Derived constructed" << endl; }
    ~Derived()
    { cout << "Derived destructed" << endl;}
};
int main() { Derived Obj(3); return 0; }

输出结果:

Base 3 constructed
Derived constructed
Derived destructed
Base 3 destructed

##封闭派生类的构造函数
封闭类的构造用初始化列表,派生类也用初始化列表,那么封闭派生类呢?
还是初始化列表

class Bug {
    private :
    int nLegs; int nColor;
    public:
    int nType;
    Bug ( int legs, int color);
    void PrintBug (){ };
};

class Skill {
    public:
    Skill(int n) { }
};
class FlyBug: public Bug {
    int nWings;
    Skill sk1, sk2;
    public:
    FlyBug( int legs, int color, int wings);
};
FlyBug::FlyBug( int legs, int color, int wings):
    Bug(legs,color),sk1(5),sk2(color) ,nWings(wings) { //初始化列表,不能访问的通通交给下层构造函数
}

封闭派生类的构造析构时序

在创建派生类的对象时:

  1. 先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员
  2. 再执行成员对象类的构造函数,用以初始化派生类对象中成员对象
  3. 最后执行派生类自己的构造函数
    在派生类对象消亡时:
  4. 先执行派生类自己的析构函数
  5. 再依次执行各成员对象类的析构函数
  6. 最后执行基类的析构函数
    析构函数的调用顺序与构造函数的调用顺序相反

    类的复合

    在数学上,两个集合有无关、相交和包含的关系。对于多个类来说,也应该有以上三种关系。无关类=两个成员不相关的类;继承类=类成员间有继承关系的类;那么相交的类呢?

    复合关系的概念

    C++用“复合”表示类的相交关系。

1)继承:“是”的关系
基类是A, B是基类A的派生类,逻辑上要求:“一个B对象也是一个A对象”
2)复合:“有”的关系
类C中“有” 成员变量k,k是类D的对象,则C和D是复合关系,逻辑上要求:“D对象是C对象的固有属性或组成部分

下面比较一下继承和复合在具体设计的实例:
继承关系顶层设计例子:

  • 写了一个 CMan 类代表男人
  • 后来又发现需要一个CWoman类来代表女人
  • CWoman类和CMan类有共同之处,让CWoman类从CMan类派生而来,是否合适?
  • 错!从一开始就应该设计CHuman类,代表“人” ,然后CMan和CWoman都从
    CHuman派生

继承逻辑关系:
image-20221208165210195

复合关系顶层设计例子:

  • 几何形体程序中,需要写“点”类,也需要写“圆”类
  • 每个圆都有圆心,那么点类应该从圆类派生出来吗?
  • 错!”点“不仅在圆内有,在其他图形也有,不是圆独有,非继承关系
  • 实际上,圆和点是复合关系,每一个“圆”对象里都包含()一个“点”对象
  • 逻辑上,复合关系就是,我的一部分可以看成是你的,但是我的全部东西不都属于你

复合关系的类通常用友元实现:

class CPoint
{
    double x,y;
    friend class CCircle;
    //便于Ccirle类操作其圆心
};

class CCircle
{
    double r;
    CPoint center;
};

复合关系的典型示例

如果要写一个小区养狗管理程序,需要写一个“业主”类,还需要写一个“狗” 类
狗是归宿于业主的,一个业主可以有多条狗,狗也可以随时脱离业主
考虑以下设计方法:
设计人和狗两个类,相互包含对方类

class CDog;
class CMaster
{
    CDog dogs[10];
};
class CDog
{
    CMaster m;
};

这样有循环定义错误!且逻辑上,狗和人并非相互包含关系
这种关系上相互相关,对象本身又完全独立的情况,用对象指针表示

class CMaster; //CMaster必须提前声明,不能先写CMaster类后写Cdog类
class CDog {
    CMaster * pm;
};
class CMaster {
    CDog * dogs[10];
};

逻辑关系:
image-20221208165220338

继承的不足

继承方式的访问限制

基类和派生类是包含的关系,那么基类对象和派生类对象是什么关系?
对于类的public派生方式:

class base { };
class derived : public base { };
base b;
derived d;

1)派生类的对象可以赋值给基类对象
b = d;
2)派生类对象可以初始化基类引用
base & br = d;
3)派生类对象的地址可以赋值给基类指针
base * pb = & d;
如果派生方式是 private或protected,则上述三条不可行

对于类的protected和private派生方式:

class base {};
class derived : protected base {};
base b;
derived d;

• protected继承时,基类的public成员和protected成员成为派生类的protected成员。
• private继承时,基类的public成员成为派生类的private成员,基类的protected成员成为派生类的不可访问成员。
• protected和private继承不是“是”的关系

派生类的对象指针转换

public派生的情况下,派生类对象的指针可以直接赋值给基类指针

Base * ptrBase = &objDerived;
//ptrBase指向的是一个Derived类的对象;

*ptrBase可以看作一个Base类的对象,访问它的public成员直接通过ptrBase即可,但不能通过ptrBase访问objDerived对象中属于Derived类而不属于Base类的成员
过强制指针类型转换,可以把ptrBase转换成Derived类的指针

Base * ptrBase = &objDerived;
Derived *ptrDerived = (Derived * ) ptrBase;

程序员要保证ptrBase指向的是一个Derived类的对象,否则很容易会错

派生类的指针赋值给基类后,基类指针也不能访问派生类的特有成员

#include <iostream>
using namespace std;
class Base {
    protected:
    int n;
    public:
    Base(int i):n(i){cout << "Base " << n <<" constructed" << endl; }
    ~Base() {cout << "Base " << n <<" destructed" << endl;}
    void Print() { cout << "Base:n=" << n << endl;}
};

class Derived:public Base {
    public:
    int v;
    Derived(int i):Base(i),v(2 * i) {
    cout << "Derived constructed" << endl;
}

~Derived() {
    cout << "Derived destructed" << endl;
}

void Func() { } ;
    void Print() {
        cout << "Derived:v=" << v << endl;
        cout << "Derived:n=" << n << endl;
    }
};

int main() {
    Base objBase(5);
    Derived objDerived(3);
    Base * pBase = & objDerived ;
    //pBase->Func(); //err;Base类没有Func()成员函数
    //pBase->v = 5; //err; Base类没有v成员变量
    pBase->Print();
    //Derived * pDerived = & objBase; //error
    Derived * pDerived = (Derived *)(& objBase);
    pDerived->Print(); //慎用,可能出现不可预期的错误
    pDerived->v = 128; //往别人的空间里写入数据,会有问题
    objDerived.Print();
    return 0;
}

输出:

Base 5 constructed
Base 3 constructed
Derived constructed
Base:n=3
Derived:v=1245104 //pDerived->n 位于别人的空间里
Derived:n=5
Derived:v=6
Derived:n=3
Derived destructed
Base 3 destructed
Base 5 destructed

从逻辑上来说,派生类指针既然能被赋值给基类指针,那么通过基类指针,应该能调用派生类的成员函数,获取派生类的成员变量。在下一章,继承类的多态将实现这个目的。

多级继承

类A派生类B,类B派生类C,类C派生类D……
– 类A是类B的直接基类
– 类B是类C的直接基类,类A是类C的间接基类
– 类C是类D的直接基类,类A、 B是类D的间接基类
在声明派生类时, 只需要列出它的直接基类
– 派生类沿着类的层次自动向上继承它的间接基类
– 派生类的成员包括
• 派生类自己定义的成员
• 直接基类中的所有成员
• 所有间接基类的全部成员

多态:在继承上更进一步

前面派生类的对象指针转换一节,基类指针强转后也不能访问派生类私有对象。考虑一下本文开始讲的交通工具顶层设计思路,在顶层设计时就要设计类的成员函数,在派生类也要设计成员函数,这些函数会有重合的情况吗?如果有重合,基类指针也不能访问派生类成员,这样基类和派生类不就失去联系了吗?多级继承这种情况不是更加严重?
为了解决这种问题,本节引入继承类的“多态”
多态能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少

虚函数与多态

在类的定义中,前面有 virtual 关键字的成员函数就是虚函数

class base {
    virtual int get() ;
};
int base::get(){ }

virtual关键字只用在类定义里的函数声明中使用,定义函数体时不用。
使用虚函数,来实现“多态”效果。多态有通过指针和引用两种表现形式:

  • 能通过基类的指针调用派生类虚函数,访问其特有成员变量

派生类的指针可以赋给基类指针
通过基类指针调用基类和派生类中的同名虚函数时:
(1)若该指针指向一个基类的对象,那么被调用是
基类的虚函数;
(2)若该指针指向一个派生类的对象,那么被调用
的是派生类的虚函数

class CBase {
public:
    virtual void SomeVirtualFunction() { }
};
class CDerived:public CBase {
public :
    virtual void SomeVirtualFunction() { }
};
int main() {
    CDerived ODerived;
    CBase * p = & ODerived;
    p -> SomeVirtualFunction(); //调用哪个虚函数取决于p指向哪种类型的对象
    return 0;
} 
  • 能通过基类的引用调用派生类虚函数

派生类的对象可以赋给基类引用
通过基类引用调用基类和派生类中的同名虚函数时:
(1)若该引用引用的是一个基类的对象,那么被调
用是基类的虚函数;
(2)若该引用引用的是一个派生类的对象,那么被
调用的是派生类的虚函数。

class CBase {
public:
    virtual void SomeVirtualFunction() { }
};
class CDerived:public CBase {
public :
    virtual void SomeVirtualFunction() { }
};
int main() {
    CDerived ODerived;
    CBase & r = ODerived;
    r.SomeVirtualFunction(); //调用哪个虚函数取决于r引用哪种类型的对象
    return 0;
} 

是不是所有成员函数加virtual都是多态?不是!

  • 在非构造或析构函数的成员函数中调用虚函数,是多态。在运行时才确定到底调用哪一层派生类函数
  • 在构造函数和析构函数中调用虚函数,不是多态。调用的函数是当前类的函数,编译时即确定

多层继承实现多态,每一层都要加virtual关键字吗?

  • 派生类中和基类中虚函数同名同参数表的函数,不加virtual也自动成为虚函数

多态与对象指针

一个变量有两方面属性:类型、值
那么多态把derived类的地址值,赋值给base类的指针,访问对象成员时是什么效果?
以下例子的this指针指向什么?

class Base {
public:
    void fun1() { this->fun2(); } //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();
    return 0;
}

pBase被Derived对象的地址赋值后,其值为Derived对象的地址,但类型还是Base的指针(多态指针赋值不会强转)。pBase->fun1()会先在Base类访问其fun1(),传入this指针(指向fun2),而this->fun2()会调用Derived类的fun2()
输出:

Derived:fun2()

虚函数也可以定义为private:

class Base {
private:
    virtual void fun2() { cout << "Base::fun2()" << endl; }
};
class Derived:public Base {
public:
    virtual void fun2() { cout << "Derived:fun2()" << endl; }
};
Derived d;
Base * pBase = & d;
pBase -> fun2(); // 编译出错

pBase已经被赋值为指向derived d的指针,不能调用base类的private函数。

多态的实例:游戏开发

游戏中有很多种怪物,每种怪物都有一个类与之对应。某个玩家创建的具体怪物就是对象
怪物的主要动作(成员函数)有:

  • 攻击(Attack),针对不同的被攻击者有不同的函数
  • 反击(FightBack),被某个怪物攻击时做出的相应动作
  • 掉血(Hurted),被攻击时会掉血,血量值不同有不同处理,如死亡

现在的需求是:已经有CWolf、CGhost两种怪物,需要设计新的怪物CThunderBird,并能满足和其他怪物的交互
顶层设计:
设置基类 CCreature,并且使CDragon, CWolf等其他类都从CCreature派生而来
image-20221208165241029
非多态的派生类设计:
由于每个怪物对于其他怪物的攻击和反击都是不同的,每个怪物类都要设计一组Attack和FightBack:

class class CCreature {
    protected: int nPower ; //代表攻击力
    int nLifeValue ; //代表生命值
};
class CThunderBird : public CCreature {
    public:
    void Attack(CWolf * pWolf) {
        ...表现攻击动作的代码
        pWolf->Hurted( nPower);
        pWolf->FightBack( this);
    }
    void Attack( CDragon * pDragon) {
        ...表现攻击动作的代码
        pDragon->Hurted( nPower);
        pDragon->FightBack( this);
    }
    void FightBack( CWolf * pWolf) {
        ....表现反击动作的代码
        pWolf ->Hurted( nPower / 2);
    }
    void FightBack( CDragon * pDragon) {
        ....表现反击动作的代码
        pDragon->Hurted( nPower / 2 );
    }
    void Hurted ( int nPower) {
        ....表现受伤动作的代码
        nLifeValue -= nPower;
    }
}

现有n种怪物,CThunderBird类中就得有n个Attack 和n个FightBack成员函数,对于其他类也得新增针对CThunderBird的Attack和FightBack。这种设计工作量过于巨大。原因就在于要区分传入的对象指针。
那么能否传入基类的指针呢,这样就不存在为各种类型写几个函数。基类指针要访问派生类的成员,得用虚函数形成多态。多态实现如下:

//基类 CCreature:
class CCreature {
protected :
    int m_nLifeValue, m_nPower;
    public:
    virtual void Attack( CCreature * pCreature) {}
    virtual void Hurted( int nPower) { }
    virtual void FightBack( CCreature * pCreature) {}
};
//派生类 CDragon:
class CDragon : public CCreature {
public:
    virtual void Attack( CCreature * pCreature);
    virtual void Hurted( int nPower);
    virtual void FightBack( CCreature * pCreature);
};

//派生类的成员函数实现具体操作
void CDragon::Attack(CCreature * p) //传入基类指针
{ …表现攻击动作的代码
    p->Hurted(m_nPower); //多态
    p->FightBack(this); //多态
}
void CDragon::Hurted( int nPower)
{ …表现受伤动作的代码
    m_nLifeValue -= nPower;
}
void CDragon::FightBack(CCreature * p)
{ …表现反击动作的代码
    p->Hurted(m_nPower/2); //多态
}

//多态的调用
CDragon Dragon; CWolf Wolf; CGhost Ghost;
CThunderBird Bird;
Dragon.Attack( & Wolf); //调用CWolf::Hurted
Dragon.Attack( & Ghost); //调用CGhost::Hurted
Dragon.Attack( & Bird); //调用CBird::Hurted

使用多态,新增某个派生类时,已有的类可以原封不动,因为传入基类指针,会“自动”调用正确的派生类函数,开发者只需要设计新增的派生类和其成员函数即可

多态的原理:虚函数表指针

多态” 的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定,这叫“动态联编”
首先分析包含虚函数的类对象的内存分布:

class Base {
public:
int i;
    virtual void Print() { cout << "Base:Print" ; }
};

class Derived : public Base{
public:
int n;
    virtual void Print() { cout <<"Drived:Print" << endl; }
};

int main() {
    Derived d;
    cout << sizeof( Base) << ","<< sizeof( Derived ) ;
    return 0;
}

输出:8, 12
为什么类对象的size比成员变量int(4字节)还多4字节?
因为包含虚函数的基类,实例化的对象除了成员变量,还包含一个指针(一般4字节),指向虚函数的入口地址,如果有多个虚函数,这些地址连续排列形成虚函数表,指针指向首个虚函数地址。如果这个指针指向基类,就能找到基类的所有虚函数入口,如果指针指向派生类,就能找到派生类的的所有虚函数入口。基类和派生类对象的指针赋值,实际会导致虚函数表指针指向的虚函数入口地址不同,从而调用时不同。
如果当前指针指向基类,则调用基类自己的虚函数:

Base b;
pBase = &b;
pBase->Print();

image-20221208165259927
如果当前指针指向派生类,则调用派生类的虚函数:

Derived d;
pDerived = &d;
pBase = pDerived;
pBase->Print();

image-20221208165311145
动态联编的实现:
多态的函数调用语句被编译成一系列根据基类指针所指向的(或基类引用所引用的)对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数的指令
而普通函数是编译过程中确定了成员函数的入口地址,不存在运行时根据对象来改变某个函数的入口地址。

虚函数与抽象类

可以想象得到,前文的游戏使用虚函数的例子是通用的,先设计基类,提炼对象属性,定义虚函数;再派生子类,在子类实现局函数的具体操作。那么问题来了,基类的虚函数有必要实现函数体吗?
很多情况,基类只是一个抽象,定义了函数的名称和参数,不需要在基类实现虚函数,全部交给派生类实现。

  • 纯虚函数:没有函数体的虚函数
  • 抽象类:包含纯虚函数的类

纯虚函数写法:没函数体{},直接=0

class A {
private: int a;
public:
    virtual void Print( ) = 0 ; //纯虚函数
    void fun() { cout << "fun"; }
};

抽象类特点:

  • 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象
  • 可以创建抽象类的指针和引用,它们可以指向派生类的对象

抽象类的指针:

A a ; // 错, A 是抽象类,不能创建对象
A * pa ; // ok,可以定义抽象类的指针和引用
pa = new A ; //错误, A 是抽象类,不能创建对象

如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类
抽象类的成员函数可以调用纯虚函数,但是构造函数或析构函数内不能调用纯虚函数

class A {
public:
    virtual void f() = 0; //纯虚函数
    void g( ) { 
        this->f( ) ; //ok
    }
    A( ){ 
        f( ); // 错误
    }
};
class B:public A{
public:
    void f(){cout<<"B:f()"<<endl; }
};

虚函数与构造析构函数

前面考虑了普通成员函数加virtual,可以形成虚函数达到继承类的多态效果。那么构造函数和析构函数呢?

  • 不允许以虚函数作为构造函数
  • 类继承需要把基类的析构函数设为虚函数

对于常规析构函数,通过基类指针删除派生类对象时,只能调用基类的析构函数。但是合理的做法是,应该先调用派生类的析构函数,然后调用基类的析构函数。解决的方法:把析构函数定义为virtual,由于基类析构函数是虚函数,派生类的同名析构函数自然也是虚函数。
什么时候定义虚析构函数

  • 一个类只要定义了虚函数,则应该将析构函数也定义成虚函数
  • 一个类打算作为基类使用,则应该将析构函数定义成虚函数

虚析构函数用法:通过基类的指针删除派生类对象,会首先调用派生类的析构函数,然后调用基类的析构函数

class son{
public:
    virtual ~son() {cout<<"bye from son"<<endl;};
};
class grandson:public son{
public:
    ~grandson(){cout<<"bye from grandson"<<endl;};
};
int main() {
    son *pson;
    pson= new grandson(); //pson指向派生类grandson
    delete pson;
    return 0;
}

输出:

bye from grandson
bye from son

0.概述

C++,加的到底是什么?
除了基础语法的补充和优化,C++另外几个核心特点是:

  • 面向对象设计的支持:

    类和对象对变量和函数的封装
    类和类之间的继承
    继承关系的类之间的函数调用的多态

  • 数据结构和算法的支持
    STL和各种常用数据类型

  • 高可复用、可拓展的支持
    类模板,函数模板
    函数、运算符的重载

本文内容:

  • 面向对象设计的概念
  • 类和对象的概念及使用
  • 类的几种构造函数
  • 类的析构函数
  • 类对象的this指针
  • 类的嵌套:封闭类
  • 成员的属性:友元和常量成员

面向对象设计的概念

面向过程设计的不足

程序 = 数据结构 + 算法
程序由全局变量以及众多相互调用的函数组成,算法以函数的形式实现,用于对数据结构进行操作。
结构化程序设计风格中,变量和函数的关系:
image-20221208164648489
其缺陷在于:

  • 结构化程序设计中,函数和其所操作的数据结构,没有直观的联系
  • 随着程序规模的增加,程序逐渐难以理解:
    某个数据结构到底有哪些函数可以对它进行操作?
    某个函数到底是用来操作哪些数据结构的?
    任何两个函数之间存在怎样的调用关系?
  • 结构化程序设计难以维护:
    由于没有“封装”和“隐藏”的概念,要访问某个数据结构中的某个变量,就可以直接访问,那么当该变量的定义有改动的时候,就要把所有访问该变量的语句找出来修改,不利于程序的维护、扩充。
  • 结构化程序设计难以查错:
    当某个数据结构的值不正确时,难以找出到底是那个函数导致的。
  • 结构化程序设计难以重用:
    在编写某个程序时,发现其需要的某项功能,在现有的某个程序里已经有了相同或类似的实现,那么自然希望能够将那部分代码抽取出来,在新程序中使用。在结构化程序设计中,随着程序规模的增大,由于程序大量函数、变量之间的关系错综复杂,要抽取这部分代码,会变得十分困难。

    面向对象的程序设计

    面向对象的程序设计方法,能够较好解决上述问题
    面向对象的程序 = 类 + 类 + …+ 类
    设计程序的过程,就是设计类(class)的过程
    面向对象的程序设计方法:
  • 将某类客观事物共同特点(属性)归纳出来,形成一个数据结构(可以用多个变量描述事物的属性)
  • 将这类事物所能进行的行为也归纳出来,形成一个个函数,这些函数可以用来操作数据结构(这一步叫“ 抽象”)
  • 然后,通过某种语法形式,将数据结构和操作该数据结构的函数“捆绑”在一起,形成一个“ 类”,从而使得数据结构和操作该数据结构的算法呈现出显而易见的紧密关系,这就是“封装
  • 类与类直接又形成继承、多态等关系
  • 面向对象的程序设计具有“抽象”,“封装”“继承”“多态”四个基本特点。

面向对象设计风格中,变量和函数的关系;
image-20221208164703654

语言和风格的无关性

注意面向过程、面向对象以及其他的风格(如函数式编程等),只是编程风格,其本质都是组织数据结构(事物属性)和算法(对事物的操作)。
C++有原生的类的概念,更方便写出面向对象风格的程序
Q. C语言没有类,能不能写出面向对象?
可以,C的结构体就是对数据的封装,配合函数指针,也能包含函数成员。利用带函数指针的结构体能实现属性和方法的封装,在Linux内核和设备驱动程序中充满了这种面向对象设计风格。事实上,C++的class在编译器处理后就是类似于C的结构体。
Q. 什么时候应该面向对象?
面向对象对于人的抽象概括的能力要求较高,需要花较多精力在top-down的顶层设计中,通常用于大型的长期维护的程序设计。
面向对象的优势在于数据结构组织化,程序时间和空间的开销可能不如面向过程。例如一个对象里的各个数据的生命周期都是捆绑分配和释放的,而面向过程可以更精细管理。在极端资源紧缺的情况,如部分嵌入式开发,面向过程不论代码设计速度和性能都比面向对象好。

类和对象的概念

类和对象的定义

设计一个程序,接受输入矩形的长和宽,输出面积和周长
如何用类来封装?

  • 矩形的属性就是长和宽。因此需要两个变量,分别代表长和宽
  • 矩形的操作方法可以有设置长和宽,算面积,算周长。每个操作各用一个函数来实现,且函数都需要用到长和宽这两个属性
  • 将以上属性和方法组合就能形成一个“矩形类”。长、宽变量成为该“矩形类”的“成员变量”,三个函数成为该类的“成员函数”。成员变量和成员函数统称为类的成员。“类”看上去就像“带函数的结构”

类的声明:

class CRectangle
{
    public:
        int w, h;
        int Area() {
        return w * h;
    }
    int Perimeter(){
        return 2 * ( w + h);
    }
    void Init( int w_,int h_ ) {
        w = w_; h = h_;
    }
}; //必须有分号

类的实例化:

int main( )
{
    int w,h;
    CRectangle r; //r是一个对象
    cin >> w >> h;
    r.Init( w,h);
    cout << r.Area() << endl <<
    r.Perimeter();
    return 0;
}

通过类,可以定义变量。类定义出来的变量,也称为类的实例,就是“对象”,对象的本质是在内存中分配了一个存放类这个结构的空间。
C++中,类的名字就是用户自定义的类型的名字。可以像使用基本类型那样来使用。 CRectangle就是一种用户自定义的类型。

对象的内存分配

  • 和结构变量一样,对象所占用的内存空间的大小,等于所有成员变量的大小之和(考虑内存对齐可能更大)。对于上面的CRectangle类,sizeof(CRectangle)
    = 8

  • 每个对象各有自己的存储空间。一个对象的某个成员变量被改变了,不会影响到另一个对象

  • 和结构变量一样,对象之间可以用 “=”进行赋值,但是不能用 “==”“!=”“>”“<”“>=”“<=”进行比较,除非这些运算符经过了“重载”。

Q.类分配内存产生对象后,成员变量占用空间,成员函数占不占用空间?
普通成员函数不在对象生成时分配函数空间,因为函数是静态绑定的,即函数体指令只占用代码段的一处空间,对象调用该函数之间跳到该空间入口地址,在对象分配时不会在堆或栈再开辟空间存放函数体。
但是当类中定义了虚函数,分配对象时要分配4字节(多个虚函数也是4个字节)的指针指向虚函数表。函数跳转地址依赖于运行时才产生的对象里的虚函数表,称为动态绑定,对象调用虚函数时不知道准确的跳转地址,只跳转到虚函数表查找跳转地址,再根据查找结果跳转。

对象访问其成员

类似于C结构体实例访问其成员的方法,用实例.成员,实例指针->成员,除此之外C++特有的通过引用访问:实例引用.成员
用法1:对象名.成员名

CRectangle r1,r2;
r1.w = 5;
r2.Init(5,4);

Init函数作用在 r2 上,即Init函数执行期间访问的w 和 h是属于r2 这个对象的, 执行r2.Init 不会影响到r1
用法2. 指针->成员名

CRectangle r1,r2;
CRectangle * p1 = & r1;
CRectangle * p2 = & r2;
p1->w = 5;
p2->Init(5,4); //Init作用在p2指向的对象上

用法3:引用名.成员名

CRectangle r2;
CRectangle & rr = r2;
rr.w = 5;
rr.Init(5,4); //rr的值变了, r2的值也变

类成员的访问方式

类成员的访问控制

Q. C++将数据和函数封装成类的成员,那么类内成员、内间成员的访问权限如何控制?
用下列访问范围关键字来说明类成员可被访问的范围:

  • private: 私有成员,只能在成员函数内访问
  • public : 公有成员,可以在任何地方访问
  • protected: 保护成员,用于继承关系的类的成员访问控制

定义一个带访问控制的类:

class className {
    private:
    私有属性和函数
    public:
    公有属性和函数
    protected:
    保护属性和函数
};

如过某个成员前面没有上述关键字,则缺省地被认为是private私有成员:

class Man {
    int nAge;       //私有成员
    char szName[20]; // 私有成员
public:
    void SetName(char * szName){
    strcpy( Man::szName,szName);
    }
};

在类的成员函数内部,能够访问:

  • 当前对象的全部属性、 函数;
  • 同类其它对象的全部属性、函数。

在类的成员函数以外的地方,只能够访问该类对象的公有成员
注意:
通过对象的成员函数,可以访问同类其他对象的任意成员(即使是private)。private、public、protected真正的作用是限制成员变量的直接访问,而通过成员函数来访问成员变量是不受影响的。

访问控制与隐藏

成员访问控制可以定义类的成员变量能否被任意访问、或通过成员函数访问、能否被继承的子类访问等。这种机制称为对成员变量的隐藏
隐藏的目的是强制对成员变量的访问一定要通过成员函数进行,那么以后成员变量的类型等属性修改后,只需要更改成员函数即可。否则所有直接访问成员变量的语句都需要修改
一个类成员变量隐藏的例子:

 //类定义
    class CEmployee {
    private:
        char szName[30]; //名字
    public :
        int salary; //工资
        void setName(char * name);
        void getName(char * name);
        void averageSalary(CEmployee e1,CEmployee e2);
    };
    
    //成员函数定义
    void CEmployee::setName( char * name) {
        strcpy( szName, name); //ok
    }
    void CEmployee::getName( char * name) {
        strcpy( name,szName); //ok
    }
    void CEmployee::averageSalary(CEmployee e1,CEmployee e2){
        cout << e1.szName; //ok,访问同类其他对象私有成员
        salary = (e1.salary + e2.salary )/2;
    }
    
    //使用类和对象
    int main()
    {
        CEmployee e;
        strcpy(e.szName,"Tom1234567889"); //编译错,不能访问私有成员
        e.setName( "Tom");  // ok
        e.salary = 5000;    //ok
        return 0;
    }

如果将上面的程序移植到内存空间紧张的设备上,希望将szName改为char szName[5],若szName不是私有,就要找出所有类似strcpy(e.szName,”Tom1234567889”);这样的语句进行修改,以防止数组越界。如果将szName变为私有,那么程序中就不可能出现(除非在类的内部)strcpy(e.szName,”Tom1234567889”);这样的语句,所有对szName的访问都是通过成员函数来进行,比如:e.setName( “Tom12345678909887”);如果szName改短了,上面的语句也不需要找出来修改,只要改setName成员函数,在里面确保不越界就可以了
除了使用类和隐藏机制,C++兼容C的struct结构体,也称为类。和用”class”的唯一区别是未说明是公有还是私有的成员,struct类的所有成员都是公有的。

struct CEmployee {
    char szName[30]; //公有!!
    public :
    int salary; //工资
    void setName(char * name);
    void getName(char * name);
    void averageSalary(CEmployee
    e1,CEmployee e2);
};

类成员函数的重载和缺省参数

同普通函数一样,类封装后的成员函数可以重载,可以有缺省参数

class Location {
    private :
    int x, y;
    public:
    void init( int x=0 , int y = 0 );
    void valueX( int val ) { x = val ;}
    int valueX() { return x; }
};

void Location::init( int X, int Y)
{
    x = X;
    y = Y;
}

int main() {
    Location A,B;
    A.init(5);  //使用init缺省y=0
    A.valueX(5);    //重载,使用valueX(int)
    cout << A.valueX();     //重载,使用valueX()
    return 0;
}

输出:5
注意:重载和缺省的函数在调用时可能冲突,存在二义性:

class Location {
    private :
    int x, y;
    public:
    void init( int x =0, int y = 0 );
    void valueX( int val = 0) { x = val; }
    int valueX() { return x; }
};

Location A;
A.valueX(); //错误,编译器无法判断调用哪个valueX

类对象的创建与释放

普通构造函数

定义一个类只是定义一种数据结构类型,类实例化后在内存中才存在改类的对象。类实例化成对象可以在函数的栈中,或者动态分配在堆中

ClassA a;   //该语句在函数内(如main)时,在main的堆栈中分配内存
ClassA *pa = new ClassA;    //在堆中分配,需要delete手动释放

那么问题来了,分配的内存里的内容是什么?
不知道是什么值,只知道这块内存是被其他进程释放过,当前程序可以读写,释放时不会把值清零。
在C语言创建一个结构体变量,可以顺便初始化为全0

StructA a = {0}; //单层结构体
StructB b = {{0}}; //嵌套的结构体

C++也支持创建类时自动初始化,采用与类同名的成员函数的方法。这就是构造函数(constructor)
构造函数:

  • 成员函数的一种,名字与类名相同,可以有参数,不能有返回值(void也不行)
  • 作用是对对象进行初始化,如给成员变量赋初值
  • 如果定义类时没写构造函数,则编译器生成一个默认的无参数的构造函数。默认构造函数无参数,不做任何操作
  • 如果定义了构造函数,则编译器不生成默认的无参数构造函数
  • 对象生成时构造函数自动被调用。对象一旦生成,就再也不能在其上执行构造函数
  • 一个类可以有多个构造函数,即构造函数也可以重载

注意:构造函数不负责对象的内存分配,其关键作用是对象成员的值初始化。真正做对象分配的语句通常是new,new做两件事:给类分配内存形成对象,调用对象的构造函数。考虑一下也可知道,连对象都没有的情况,怎么能调用对象的构造函数分配内存呢?注意构造函数不给自身对象分配内存,但是构造函数可以做分配内存操作,比如对指针成员指向的空间分配内存。
使用默认构造函数:

class Complex {
    private :
    double real, imag;
    public:
    void Set( double r, double i);
}; //编译器自动生成默认构造函数
Complex c1; //默认构造函数被调用
Complex * pc = new Complex; //默认构造函数被调用

使用自定义的带参构造函数:

class Complex {
    private :
    double real, imag;
    public:
    Complex( double r, double i = 0);
};
    Complex::Complex( double r, double i) {
    real = r; imag = i;
}

Complex c1; // error, 缺少构造函数的参数
Complex * pc = new Complex; // error, 没有参数
Complex c1(2); // OK
Complex c1(2,4), c2(3,5);
Complex * pc = new Complex(3,4);

使用重载的构造函数:

class Complex {
    private :
    double real, imag;
    public:
    void Set( double r, double i );
    Complex(double r, double i );
    Complex (double r );
    Complex (Complex c1, Complex c2);
};

Complex::Complex(double r, double i)
{
    real = r; imag = i;
}
Complex::Complex(double r)
{
    real = r; imag = 0;
}
Complex::Complex (Complex c1, Complex c2);
{
    real = c1.real+c2.real;
    imag = c1.imag+c2.imag;
}

Complex c1(3) , c2 (1,0), c3(c1,c2);
// c1 = {3, 0}, c2 = {1, 0}, c3 = {4, 0};

构造函数应该是public的, private构造函数不能直接用来初始化对象

class CSample{
private:
    CSample() {}
};

int main(){
    CSample Obj; //err. 唯一构造函数是private
    return 0;
}

对于多个对象的实例化,可以用对象数组,构造函数的调用次数=对象个数,重载哪一个构造函数取决于每个对象的初始化方式。

class CSample {
    int x;
    public:
    CSample() {
        cout << "Constructor 1 Called" << endl;
    }
    CSample(int n) {
        x = n;
        cout << "Constructor 2 Called" << endl;
    }
};

int main(){
    CSample array1[2];  //两次默认构造函数
    cout << "step1"<<endl;
    CSample array2[2] = {4,5};  //两次带参构造函数
    cout << "step2"<<endl;
    CSample array3[2] = {3};    //第一个带参构造,第二个默认构造
    cout << "step3"<<endl;
    CSample * array4 = new CSample[2];  //两次默认构造
    delete []array4;
    return 0;
}

输出:

Constructor 1 Called
Constructor 1 Called
step1
Constructor 2 Called
Constructor 2 Called
step2
Constructor 2 Called
Constructor 1 Called
step3
Constructor 1 Called
Constructor 1 Called

拷贝构造函数

定义:拷贝构造函数(copy constructor)是构造函数的一种,特点是:

  • 只有一个参数:对同类对象的引用
  • 入参必须是对象的引用,形如 X::X( X& ) 或 X::X(const X &), 后者以常量对象作为参数
  • 如果用户没有定义拷贝构造函数,编译器生成默认的拷贝构造函数,且它完成复制对象的功能。

拷贝构造函数也称为复制构造函数
调用形式如下。默认(普通)构造函数和默认拷贝构造函数都是编译生成,且并存的

class Complex {
private :
    double real,imag;
};
Complex c1; //调用缺省无参构造函数
Complex c2(c1);//调用缺省的复制构造函数,将 c2 初始化成和c1一样

如果定义的自己的拷贝构造函数,则默认的拷贝构造函数不会生成
也就是说,自定义的带参拷贝构造函数和编译器生成的默认拷贝构造函数,不存在重载关系;而一个类有多个自定义的带参拷贝构造函数是允许的,可以重载。这一特点对于普通构造函数一样。

class Complex {
public :
    double real,imag;
    Complex(){ }
    Complex( const Complex & c ) {
        real = c.real;
        imag = c.imag;
        cout << “Copy Constructor called”;
    }
};
Complex c1;
Complex c2(c1); //调用自己定义的复制构造函数,输出 Copy Constructor called

注意:拷贝构造函数传入的是同类的引用,而不是同类的对象
不允许有形如 X::X( X)的构造函数。因为成员函数入参由实参复制到形参实际会调用拷贝构造函数,拷贝构造函数作为成员函数也是一样,因此会有循环定义,即拷贝构造函数的执行需要调用拷贝构造函数的无限循环,用引用作为入参可以解决此问题。这点类似于C结构体允许有结构体指针成员,指向该结构体类型的实例,而不允许结构体有自身结构体的自接实例,这样会照成分配内存空间上的无限循环。

class CSample {
    CSample( CSample c ) {} //错,不允许这样的构造函数
}

拷贝构造函数的调用

以下三种情况会调用类对象的拷贝构造函数
1)用一个对象去初始化同类的另一个对象:

Complex c2(c1);
Complex c2 = c1; //初始化语句,非赋值语句

2)类的对象作为函数入参:如果某函数有参数是类A的对象,那么该函数被调用时,类A的拷贝构造函数将被调用:

class A
{
public:
    A() { };
    A( A & a) {
        cout << "Copy constructor called" <<endl;
    }
};

void Func(A a1){ };
int main(){
    A a2;
    Func(a2);  //传参是类A的对象
    return 0;
}

输出: Copy constructor called
3) 类的对象作为函数返回值:如果函数的返回值是类A的对象,函数返回时,A的拷贝构造函数被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A
{
public:
int v;
A(int n) { v = n; };
A( const A & a) {
v = a.v;
cout << "Copy constructor called" <<endl;
}
};

A Func() {
A a(4);
return a;
}
int main() {
cout << Func().v << endl;
return 0;
}

输出:

Copy constructor called
4

小结:对象作为入参和返回值会调用拷贝构造函数,对象初始化新对象也会调用。

禁用拷贝构造函数

Q. 调用拷贝构造函数会形成对象的复制品,开销较大,如何禁用拷贝构造函数?
使用对象的引用,不自接把对象作为函数的入参出参。
Q.对象的引用会导致新问题:函数内修改了引用怎么办,原对象也会改
使用const引用,对象实参就不存在被函数修改的可能
使用对象的常引用,应用于对象作为函数入参出参,又不希望调用拷贝构造函数的情况

void fun(const CMyclass & obj) {
//函数中任何试图改变 obj值的语句都将是变成非法
}

对象的赋值和复制

注意区分对象的赋值和复制:

  • 对象赋值是类的所有数据成员的一一对应赋值,其本质是对已分配内存的对象,进行数据成员的初始化

  • 对象复制 = 分配新对象对象空间 + 对新对象成员的赋值初始化。对象复制是要包含空间分配操作的

两个已分配内存的对象间的赋值并不会导致拷贝构造函数被调用

//声明及初始化,调用拷贝构造函数
Complex c2 = c1; 
//先声明对象,再赋值,不调用拷贝构造函数,调用默认构造函数然后赋值
Complex c2;
c2 = c1;    

浅拷贝和深拷贝

当类对象有指针成员时,拷贝构造函数遇到一个问题,是只拷贝指针,还是连同指针指向的空间一起拷贝?

  • 浅拷贝:只拷贝指针成员
  • 深拷贝:拷贝指针成员,并拷贝其指向的内存空间数据
    由于深拷贝的实现用到“=”运算符重载,在运算符重载一节详述

转换构造函数

构造函数是能创建对象并初始化值的函数,将普通变量转换从类对象并分配内存空间的构造函数是转换构造函数。

  • 定义转换构造函数的目的是实现类型的自动转换(变量->对象)
  • 只有一个参数,且不是拷贝构造函数的构造函数,就是转换构造函数
  • 变量被赋值给对象时,编译器会自动调用转换构造函数,建立一个无名的临时对象

隐式的转换构造函数:

    class Complex {
    public:
        double real, imag;
        Complex( int i) {//类型转换构造函数
            cout << "IntConstructor called" << endl;
            real = i; imag = 0;
        }
        Complex(double r,double i) {real = r; imag = i; }
    };
    
    int main ()
    {
        Complex c1(7,8);
        Complex c2 = 12;
        c1 = 9;     // 9被自动转换成一个临时Complex对象
        cout << c1.real << "," << c1.imag << endl;
        return 0;
    }

显式的转换构造函数:

class Complex {
public:
    double real, imag;
    explicit Complex( int i) {  //显式类型转换构造函数
        cout << "IntConstructor called" << endl;
        real = i; imag = 0;
    }
    Complex(double r,double i) {real = r; imag = i; }
};
int main () {
    Complex c1(7,8);
    Complex c2 = Complex(12);
    c1 = 9;         // error, 9不能被自动转换成一个临时Complex对象
    c1 = Complex(9) //ok
    cout << c1.real << "," << c1.imag << endl;
    return 0;
}

析构函数

析构函数的概念

**析构函数(destructors)**用于对象生命周期结束前(如函数中的对象在函数返回时消失),释放对象的内存占用,以及其他的准备工作。
构造函数和析构函数在对象生命周期的角色从逻辑上讲是开始和结束的关系,但具体操作不一样:构造函数不为对象分配内存,只给成员赋初值;而析构函数一般要释放对象的内存
析构函数的特点:

  • 名字与类名相同,在前面加‘~’,没有参数和返回值,一个类最多只能有一个析构函数
  • 析构函数对象消亡时即自动被调用。可以定义析构函数来在对象消亡前做善后工作,比如释放分配的空间等。
  • 如果定义类时没写析构函数,则编译器生成缺省析构函数。缺省析构函数什么也不做
  • 如果定义了析构函数,则编译器不生成缺省析构函数

析构函数例子:

class String{
private :
    char * p;
    public:
    String () {
        p = new char[10];
    }
    ~ String () ;
};

String ::~ String()
{
    delete [] p;
}

对象数组的生命期结束时,每个对象的析构函数都会被调用。

class Ctest {
public:
    ~Ctest() { cout<< "destructor called" << endl; }
};

int main () {
    Ctest array[2];
    cout << "End Main" << endl;
    return 0;
}

输出:

End Main
destructor called
destructor called

析构函数的调用

析构函数被调用有以下几种情况
1)delete运算导致析构函数调用:

Ctest * pTest;
pTest = new Ctest;  //构造函数调用
delete pTest;       //析构函数调用
---------------------------------------------------------
pTest = new Ctest[3];   //构造函数调用3次
delete [] pTest;        //析构函数调用3次

若new一个对象数组,那么用delete释放时应该写 []。否则只delete一个对
象(调用一次析构函数)
2)析构函数在对象作为函数返回值返回后被调用。其原理是,对象作为函数的入参,出参时,都是临时生成的对象,传完就调用析构函数销毁。

class CMyclass {
public:
    ~CMyclass() { cout << "destructor" << endl; }
};
CMyclass obj;
CMyclass fun(CMyclass sobj ) { //参数对象消亡也会导致析
                                //构函数被调用
    return sobj;                //函数调用返回时生成临时对象返回
}
int main(){
    obj = fun(obj); //函数调用的返回值(临时对象)被
    return 0;       //用过后,该临时对象析构函数被调用
}

输出:

destructor
destructor
destructor

构造与析构的时序

总体原则:类似堆栈的先入后出原则:先构造的后析构
几个关键分类:
临时对象:赋值时创建,赋完值就消亡,生命周期似乎就一条指令
局部对象:在{}范围内存在,{}结束时消亡
全局、静态对象:从创建开始,在程序整个运行期间存在,程序结束时消亡。
一个例子:

class Demo {
        int id;
    public:
        Demo(int i) {
            id = i;
            cout << "id=" << id << " constructed" << endl;
        }
        ~Demo() {
            cout << "id=" << id << " destructed" << endl;
        }
};

Demo d1(1);
void Func()
{
    static Demo d2(2);
    Demo d3(3);
    cout << "func" << endl;
}

int main () {
    Demo d4(4);
    d4 = 6;
    cout << "main" << endl;
    { 
        Demo d5(5);
    }
    Func();
    cout << "main ends" << endl;
    return 0;
}

输出结果:

id=1 constructed    //全局对象d1
id=4 constructed    //构造函数d4
id=6 constructed    //转换构造函数d4
id=6 destructed     //临时对象赋值完毕,消亡
main
id=5 constructed    //构造函数d5
id=5 destructed     //d5作用域结束,消亡
id=2 constructed    //Fun构造静态对象d2(等同全局对象)
id=3 constructed    //构造局部对象d3
func
id=3 destructed     //Fun返回,d3消亡
main ends       
id=6 destructed     //Main的局部对象d4消亡(id=6)
id=2 destructed     //整个程序结束,全局对象d2消亡
id=1 destructed     //整个程序结束,全局对象d1消亡

类对象的指针:this指针

this指针是在类成员函数内,指向当前类对象的指针。
注意:

  • this指针是指向当前对象的,所谓当前,是指调用成员函数时,是通过所在的对象的指针来调用
  • this指针体现的是成员函数和对象的关系,如果是静态成员函数,没有this指针,因为静态成员函数不从属于对象

为什么this指针如此特殊,需要单独命名?这涉及到C++的类的实现原理。

C++的类与C的结构体

在C++早期,C++代码被编译器翻译成C代码,再由C编译器编译
类的实现原理和C的结构体有密切关系,下面是类和结构体的转换:
1)C++的类:

class CCar {
    public:
        int price;
        void SetPrice(int p);
};

void CCar::SetPrice(int p)
{ price = p; }

int main()
{
    CCar car;
    car.SetPrice(20000);
    return 0;
}

2)C的结构体实现类的功能

struct CCar {
    int price;
};

void SetPrice(struct CCar * this, int p)
{ this->price = p; }

int main() {
    struct CCar car;
    SetPrice( & car,
    20000);
    return 0;
}

用C实现面向对象(CCar结构体),方法(SetPrice)传入的参数是结构体对象的指针(struct CCar * this)

C++的this指针

成员函数(非static)可以直接使用this来代表指向该函数作用的对象的指针

class Complex {
public:
    double real, imag;
    void Print() { cout << real << "," << imag ; }
    Complex(double r,double i):real(r),imag(i){ }   //初始化列表
    Complex AddOne() {
        this->real ++;  //等价于 real++
        this->Print();  //等价于 Print()
        return * this;
    }
};

int main() {
    Complex c1(1,1),c2(0,0);
    c2 = c1.AddOne();
    return 0;
} //输出 2,1

对象的this指针通常隐式存在:

  • 成员函数(非static)的入参实际隐式地有一个this指针参数
  • 成员函数访问成员变量,也是隐式的通过this指针访问
  • 通过对象的指针调用成员函数,本质也是传入this指针

如果成员函数不访问成员变量,可以传入NULL的对象指针:

class A
{
    int i;
    public:
    void Hello() { cout << "hello" << endl; }
};  //等价于 void Hello(A * this ) { cout << "hello" << endl; }
int main()
{
    A * p = NULL;
    p->Hello(); //等价于Hello(p)
} // 输出: hello

如果成员函数访问了成员变量,实际是通过成员函数传入的this指针来访问,此时指针不可为NULL

class A
{
    int i;
    public:
    void Hello() { cout << i << "hello" << endl; }
};  
//等价于void Hello(A * this ) { cout << this->i << "hello" << endl; }
//this若为NULL,则出错!!
int main()
{
    A * p = NULL;
    p->Hello(); //等价于Hello(p);
} //出错

静态成员的概念

静态成员:在定义前面加了static关键字的成员、

class CRectangle
{
    private:
    int w, h;
    static int nTotalArea; //静态成员变量
    static int nTotalNumber;
    public:
    CRectangle(int w_,int h_);
    ~CRectangle();
    static void PrintTotal(); //静态成员函数
};
  • 普通成员变量每个对象有各自的一份;而静态成员变量是全局共有的一份,为所有对象共享
  • 同一个类的成员函数,不论静不静态都是一份代码段
  • 普通成员函数必须具体作用于某个对象(也可以理解为绑定),而静态成员函数并不具体作用于某个对象
  • 因此静态成员(变量或者函数),不需要通过对象就能访问

sizeof求类大小,不会计算静态成员变量,因为不属于类的一部分(从空间占用上讲)。

class CMyclass {
int n;
static int s;
};  // sizeof(CMyclass) 等于 4

访问静态成员

一下几种方法访问,可以归纳为两种:通过类名访问,通过对象访问
1)类名::成员名

CRectangle::PrintTotal();

2)对象名.成员名

CRectangle r; 
r.PrintTotal();

3)指针->成员名

CRectangle * p = &r; 
p->PrintTotal();

4)引用.成员名

CRectangle & ref = r; 
int n = ref.nTotalNumber;

静态成员函数与this指针

  • 静态成员函数中不能使用 this 指针!
  • 因为静态成员函数并不具体作用与某个对象!
  • 因此静态成员函数的真实的参数的个数,就是程序中写出的参数个数!

前面讲,C++的作用是封装数据,静态成员似乎破坏这一目的,那么静态成员有什么作用?
为了兼容C的全局变量与函数

  • 静态成员变量本质上是全局变量,哪怕一个对象都不存在,类的静态成员变量也存在
  • 静态成员函数本质上是全局函数
  • 设置静态成员这种机制的目的是将和某些类紧密相关的全局变量和函数写到类里面,看上去像一个整体,易于维护和理解

静态成员函数的使用场景

对于需要全局维护的数据,可以使用静态成员变量,并通过静态成员函数访问。
考虑一个图形处理程序,需要随时知道矩形的总数和总面积

  • 每个矩形封装成类的对象
  • 总数和总面积是类的静态成员(等价于全局变量)

类定义:

1
2
3
4
5
6
7
8
9
10
11
class CRectangle
{
private:
int w, h;
static int nTotalArea;
static int nTotalNumber;
public:
CRectangle(int w_,int h_);
~CRectangle();
static void PrintTotal();
};

成员函数定义:

CRectangle::CRectangle(int w_,int h_)
{
    w = w_;
    h = h_;
    nTotalNumber ++;
    nTotalArea += w * h;
}
CRectangle::~CRectangle()
{
    nTotalNumber --;
    nTotalArea -= w * h;
}
void CRectangle::PrintTotal()
{
    cout << nTotalNumber << "," << nTotalArea << endl;
}

类对象的调用:

int CRectangle::nTotalNumber = 0;
int CRectangle::nTotalArea = 0;
// 必须在定义类的文件中对静态成员变量进行一次说明或初始化。否则编译能通过,链接不能通过。
int main()
{
    CRectangle r1(3,3), r2(2,2);
    //cout << CRectangle::nTotalNumber; 
    //错误 , 静态的私有变量也只能通过成员函数访问,静态不等于全局可访问
    CRectangle::PrintTotal();
    r1.PrintTotal();
    return 0;
}

输出结果:

2,13
2,13

注意两点:

  • 静态成员变量是全局共有的一份存储,但private的静态成员只能通过类的成员函数访问。注意区分全局存储和全局访问,静态成员只有全局存储特性,没有全局可访问特性。
  • 静态成员函数,不能访问非静态成员变量,也不能调用非静态成员函数。

以下静态成员函数访问错误:

void CRectangle::PrintTotal()
{
    cout << w << "," << nTotalNumber << "," << nTotalArea << endl; //错误
}
CRetangle::PrintTotal(); //解释不通 w 到底是属于那个对象的

以上例子还有缺陷:
在使用静态成员时,特别是类的构造和析构会修改该静态成员,如前文的CRectangle类的构造函数有nTotalNumber++操作,析构有nTotalNumber–。这个时候要考虑构造和析构函数是否覆盖到所有类型(普通构造,拷贝构造,转换构造)
在使用CRectangle类时,有时会调用复制构造函数生成临时的隐藏的CRectangle对象:

  • 调用一个以CRectangle类对象作为参数的函数时
  • 调用一个以CRectangle类对象作为返回值的函数时

临时对象在消亡时会调用析构函数,减少nTotalNumber和nTotalArea的值,可是这些临时对象在生成时却没有增加nTotalNumber和nTotalArea的值,因为设计类时漏掉了拷贝构造的情况
解决办法:为CRectangle类写一个拷贝构造函数:

CRectangle :: CRectangle(CRectangle & r )
{
    w = r.w; h = r.h;
    nTotalNumber ++;
    nTotalArea += w * h;
}

这样nTotalNumber和nTotalArea全局计数就是准确的

类的嵌套:封闭类

封闭类的基本概念

再来把C++的类和C结构体对比下:

  • C:结构体的成员可以是基础变量,基础变量的指针,结构体的指针,其他复合类型的指针
  • C++:类的成员变量可以是基础变量,及其指针、引用,可不可以是类对象?类对象的引用和指针?

于是引入类嵌套类对象的情况:有成员对象的类叫封闭类(enclosing class)
一个示例:写一个汽车类,包含轮胎和引擎类对象
轮胎和引擎类:

class CTyre //轮胎类
    {
    private:
        int radius; //半径
        int width; //宽度
    public:
        CTyre(int r,int w):radius(r),width(w) { }   //用初始化列表构造
    };
    
class CEngine //引擎类
{
};

汽车类:

class CCar { //汽车类
private:
    int price; //价格
    CTyre tyre;
    CEngine engine;
public:
    CCar(int p,int tr,int tw );
};
CCar::CCar(int p,int tr,int tw):price(p),tyre(tr, tw) //用初始化列表构造
{
};

汽车类的使用:

int main()
{
    CCar car(20000,17,225); //传入初始化列表
    return 0;
}

初始化列表构造封闭类

对于封闭类,有几个问题就凸显出来:

  • 构造一个封闭类,还要构造其嵌套的类
  • 构造时序是怎样的
  • 析构时序是怎样的

上例中,如果 CCar类不定义构造函数,下面的语句会编译出错:CCar car;
因为CCar不传初始化值给嵌套类CTyre,编译器不知道该如何初始化car.tyre的成员变量
而car.engine的初始化没问题,因为不用初始化成员变量,用默认构造函数即可
为了解决封闭类的嵌套类成员的初始化问题,构造函数引入新的初始化方法:

  • 初始化列表:将成员初始化从构造函数体,移到函数名后面,只是换了形式,但是方便了封闭类各嵌套类的初始化,不用开发者自己到函数体写构造函数内容
  • 成员对象初始化列表中的参数可以是任意复杂的表达式,可以包括函数,变量,只要表达式中的函数或变量有定义就行。

封闭类都是通过构造函数的初始化列表,层层传入嵌套类的构造函数:

CCar::CCar(int p,int tr,int tw):price(p),tyre(tr, tw){};
//p, tr, tw是传入的初始化值; price,tyre是CCar对象的两个成员
CCar car(20000,17,225);
//Car的price = 20000, Car的tyre的radius = 17,width = 225

上例是普通构造函数,对于封闭类的拷贝构造函数:

  • 封闭类对象是用拷贝构造函数初始化的,其成员对象也用拷贝构造函数初始化

测试用例:

class A
{
public:
    A() { cout << "default" << endl; }
    A(A & a) { cout << "copy" << endl;}
};
class B { A a; };

int main()
{
    B b1,b2(b1);
    return 0;
}

输出:

default
Copy

下面考虑封闭类构造和析构的时序

  • 封闭类对象生成时,先执行所有对象成员的构造函数,然后才执行封闭类的构造函数、
  • 对象成员的构造函数调用次序和对象成员在类中的说明次序一致,与它们在成员初始化列表中出现的次序无关
  • 当封闭类的对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数。次序和构造函数的调用次序相反

一个测试示例

class CTyre {
    public:
        CTyre() { cout << "CTyre contructor" << endl; }
        ~CTyre() { cout << "CTyre destructor" << endl; }
};
class CEngine {
    public:
        CEngine() { cout << "CEngine contructor" << endl; }
        ~CEngine() { cout << "CEngine destructor" << endl; }
};
class CCar {
    private:
        CEngine engine;
        CTyre tyre;
    public:
        CCar( ) { cout << “CCar contructor” << endl; }
        ~CCar() { cout << "CCar destructor" << endl; }
};

int main(){
CCar car;
return 0;
}

输出结果:

CEngine contructor
CTyre contructor
CCar contructor
CCar destructor
CTyre destructor
CEngine destructor

类的成员属性:友元和常量成员

友元函数和友元类

友元(friend)分为友元函数和友元类两种
一个类的private成员,只能通过类自己的成员函数访问,那么其他类的成员函数想访问这个类的private成员怎么办?友元可以解决这种需求

  1. 友元函数: 一个类的友元函数可以访问该类的私有成员
    即类A内可以声明其他类B的成员函数或者全局函数,加前缀friend,这些以friends开头的函数就可访问类A的成员。

    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
    32
    class CCar ; //提前声明 CCar类,以便后面的CDriver类使用
    class CDriver
    {
    public:
    void ModifyCar( CCar * pCar) ; //改装汽车
    };
    class CCar
    {
    private:
    int price;
    friend int MostExpensiveCar( CCar cars[], int total); //声明友元
    friend void CDriver::ModifyCar(CCar * pCar); //声明友元
    };

    void CDriver::ModifyCar( CCar * pCar)
    {
    pCar->price += 1000; //访问CCar成员,汽车改装后加价
    }

    int MostExpensiveCar( CCar cars[],int total)//求最贵汽车的价格
    {
    int tmpMax = -1;
    for( int i = 0;i < total; ++i )
    if( cars[i].price > tmpMax) //访问CCar成员
    tmpMax = cars[i].price;
    return tmpMax;
    }

    int main()
    {
    return 0;
    }

除了普通成员函数,也可以将类构造、析构函数说明为另一个类的友元

2)友元类: 如果A是B的友元类,那么A的成员函数可以访问B的私有成员
如果是类的嵌套(封闭类),声明为friend的类A可以调用自己的成员函数访问与它为friend关系的类B的私有成员,而不必调用类B的成员函数。

class CCar
{
private:
    int price;
    friend class CDriver; //声明CDriver为友元类
};
class CDriver
{
public:
    CCar myCar;
    void ModifyCar() {  //改装汽车
    myCar.price += 1000;   //因CDriver是CCar的友元类,故此处可以访问其私有成员
    }
};

int main(){ return 0; }

友元类之间的关系不能传递,不能继承。就是说A和B是friend,B和C是friend,但A和C不一定是friend。父类之间的friend关系,子类不一定能传承。

常量成员函数

如果不希望某个对象的值被改变,定义该对象的时候可以在前面加 const关键字
在类的成员函数说明后面加const关键字,则该成员函数成为常量
成员函数。
常量成员函数内部不能改变属性的值,也不能调用非常量成员函数
在定义常量成员函数和声明常量成员函数时都应该使用const 关键字。

class Sample {
private :
    int value;
    public:
    void PrintValue() const;
};
void Sample::PrintValue() const {             //此处不使用const会导致编译出错
    cout << value;
}
void Print(const Sample & o) {
    o.PrintValue(); 
}//若 PrintValue非const则编译错

以下是错误示例:

class Sample {
private :
    int value;
    public:
    void func() { };
    Sample() { }
    void SetValue() const {
        value = 0; // wrong
        func(); //wrong
    }
};
const Sample Obj;
Obj.SetValue (); //常量对象上可以使用常量成员函数

什么场景定义成常量成员函数?
如果一个成员函数中没有调用非常量成员函数,也没有修改成员变量的值,最好将其写成常量成员函数

常量成员函数的重载:
两个成员函数,名字和参数表都一样,但是一个是const,一个不是,算重载关系,而非重定义。

类的运算:运算符重载

C++定义了类,可以像基本类型那样创建、销毁、初始化。那么类和类之间的运算呢?
+、 -、 *、 /、 %、 ^、 &、 ~、 !、 |、 =、 << 、>>、 !=、
考虑以下方法实现类的运算:

  • 设计类的成员函数,支持类运算操作
  • 设计某种机制,把运算符关联成函数操作,在函数内定义具体类运算方法。进行类的运算时,形式上可以像基本类型的运算一样

例如complex_a和complex_b是两个复数对象;求两个复数的和, 希望能直接写:complex_a + complex_b
运算符重载将解决类和对象的运算需求

运算符重载的概念

运算符重载,就是对已有的运算符(C++中预定义的运算符)赋予多重的含义,使同一运算符作用于不同类型的数据时导致不同类型的行为
运算符重载的目的是:扩展C++中提供的运算符的适用范围,使之能作用于对象
期望效果:同一个运算符,对不同类型的操作数,所发生的行为不同

complex_a + complex_b //生成新的复数对象
5 + 4 = 9 //基本运算符操作

从行为上看,运算符重载类似于把运算符进行了重定义成函数操作(类似C的typedef)
运算符重载写法:

返回值类型 operator 运算符(形参表)
{
……  //定义该运算符的运算规则
}

示例:

class Complex
{
public:
    double real,imag;
    Complex( double r = 0.0, double i= 0.0):real(r),imag(i) { }
    Complex operator-(const Complex & c);
};
Complex operator+( const Complex & a, const Complex & b)
{
    return Complex( a.real+b.real,a.imag+b.imag); //返回一个临时对象
}
Complex Complex::operator-(const Complex & c)
{
    return Complex(real - c.real, imag - c.imag); //返回一个临时对象
}

int main()
{
    Complex a(4,4),b(1,1),c;
    c = a + b; //等价于c=operator+(a,b);
    cout << c.real << "," << c.imag << endl;
    cout << (a-b).real << "," << (a-b).imag << endl;
    //a-b等价于a.operator-(b)
    return 0;
}

输出:

5,5
3,3

c = a + b; 等价于c=operator+(a,b);
a-b 等价于a.operator-(b)
运算符重载的实现还是成员函数,所以是依赖于对象的。也就是说,运算符重载看上去和类、对象没啥关系,但本质上,重载的运算符是归属于某个类的,因为a-b只是表象现象,真正定义对象运算的,是a.operator-(b)成员函数。
因为运算符重载依赖对象的,因此双目运算,如+,-,在运算符重载时只需要传入另一个对象,而不需要传运算符的当前对象。
重载为成员函数时, 参数个数为运算符目数减一。
重载为普通函数时, 参数个数为运算符目数

运算符重载概念小结:

  • 运算符重载的实质是函数重载
  • 可以重载为普通函数,也可以重载为成员函数
  • 把含运算符的表达式转换成对运算符函数的调用
  • 把运算符的操作数转换成运算符函数的参数
  • 运算符被多次重载时,根据实参的类型决定调用哪个运算符函数

赋值运算符的重载

接下来的几节讲几个代表性的运算符重载。本节讲赋值运算符“=”有时候希望赋值运算符两边的类型可以不匹配,比如,把一个int类型变量赋值给一个Complex对象,或把一个 char *类型的字符串赋值给一个字符串对象,此时就需要重载赋值运算符“=”。
赋值运算符“ =”只能重载为成员函数

示例:

class String {
private:
    char * str;
    public:
    String ():str(new char[1]) { str[0] = 0;}
    const char * c_str() { return str; };
    String & operator = (const char * s);
    String::~String( ) { delete [] str; }
};
String & String::operator = (const char * s)
{ //重载“=”以使得 obj = “hello”能够成立
    delete [] str;
    str = new char[strlen(s)+1];
    strcpy( str, s);
    return * this;
}

int main()
{
    String s;
    s = "Good Luck," ; //等价于 s.operator=("Good Luck,");
    cout << s.c_str() << endl;
    // String s2 = "hello!"; //这条语句要是不注释掉就会出错
    s = "Shenzhou 8!"; //等价于 s.operator=("Shenzhou 8!");
    cout << s.c_str() << endl;
    return 0;
}

输出:

Good Luck,
Shenzhou 8!

赋值运算符与深拷贝

在类与对象的拷贝构造函数一节讲了拷贝构造函数的作用:用一个已经初始化的对象,去初始化另一个对象,具体操作是讲成员变量一一赋值。
那么更深入考虑一下:对于各种类型的成员变量,能不能达到目的?

  • 对于基础类型的成员变量,如int,char,直接赋值即可
  • 对于指针类型的成员变量,给指针赋值就Ok?需不需要给指针指向的空间也赋值?
  • 对于引用类型的成员变量,直接赋值OK?
  • 对于类对象类型的成员变量,怎么赋值?嵌套调用拷贝构造函数?

引用只是标签,可以直接拷贝,等同变量拷贝。封闭类的构造函数会嵌套调用基础类型的拷贝,直到所有成员赋值完为止。
唯一需要考虑的是包含指针类型成员的类如何拷贝
如果直接赋值指针而不分配并初始化其指向空间,效果如下:
image-20221208164831061
如不定义自己的赋值运算符,那么S1=S2实际上导致 S1.str和 S2.str
指向同一地方。
如果S1对象消亡,析构函数将释放 S1.str指向的空间,则S2消亡时还
要释放一次,就形成两次delete错误!
如果执行 S1 = “other”;会导致S2.str指向的地方被delete

为了解决以上问题,类的拷贝构造不仅要拷贝指针,还有拷贝指针指向的空间(分配新内存+拷贝)。这种带内存分配的拷贝称为深拷贝

  • 浅拷贝:只拷贝成员,对于指针成员,也只拷贝指针变量
  • 深拷贝:拷贝成员,对于指针成员,拷贝指针变量,且拷贝指针指向的内存空间

为了实现深拷贝,需要重载“=”运算符:

String & operator = (const String & s) {
    delete [] str;  //先释放指针原本指向的空间,因为新空间和原空间大小可能不一样
    str = new char[strlen( s.str)+1];   //分配指针指向的新空间
    strcpy( str,s.str); //新空间赋值初始化
    return * this;  //返回当前对象的指针
}

还有可优化的,如果传入对象就是当前对象,没必要释放又分配,直接返回即可

String & operator = (const String & s){
    if( this == & s)
        return * this;
    delete [] str;
    str = new char[strlen(s.str)+1];
    strcpy( str,s.str);
    return * this;
}

整个类设计如下:

class String {
private:
    char * str;
public:
    String ():str(new char[1]) { str[0] = 0;}
    const char * c_str() { return str; };
    String & operator = (const char * s){
        delete [] str;
        str = new char[strlen(s)+1];
        strcpy( str, s);
        return * this;
};
    ~String( ) { delete [] str; }
};

再考虑一下运算符重载函数的返回值
为什么返回String &
原因:对运算符进行重载的时候,好的风格是尽量保留运算符原本的特性
例如运算符是可以多个连续运算的

a = b = c;
(a=b)=c; //会修改a的值

分别等价于:

a.operator=(b.operator=(c));
(a.operator=(b)).operator=(c);

对于拷贝构造函数,原指针未初始化,不指向任何空间,直接分配空间在拷贝即可,写法如下:

String( String & s)
{
    str = new char[strlen(s.str)+1];
    strcpy(str,s.str);
}

流运算符的重载

C++常用的输入输出是怎么实现的?

cout << 5 << “this”;
  • cout是什么?
  • “<<”原本是位偏移运算,为什么能作用于cout?
  • “<<”怎么支持连续运算,且支持多种类型

原因就是<<被流运算类重载了。

  • cout是在iostream中定义的,ostream类的对象
  • “<<” 能用在cout上是因为,在iostream里对“ <<” 进行了重载
  • 运算符重载函数返回对象的引用,实现连续运算;多个运算符重载函数的重载,支持多种类型

实现方法:

ostream & ostream::operator<<(int n)
{
    …… //输出n的代码
    return * this;
}
ostream & ostream::operator<<(const char * s )
{
    …… //输出s的代码
    return * this;
}

cout << 5 << “this”;
等价于: cout.operator<<(5).operator<<(“this”);
一个流运算符重载的示例:
假定c是Complex复数类的对象,现在希望写“ cout << c;”,就能以“ a+bi”的形式输出c的值,写“ cin>>c;”,就能从键盘接受“ a+bi”形式的输入,并且使得c.real = a,c.imag = b

#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
class Complex {
    double real,imag;
    public:
    Complex( double r=0, double i=0):real(r),imag(i){ };
    friend ostream & operator<<( ostream & os, const Complex & c);
    friend istream & operator>>( istream & is,Complex & c);
};
ostream & operator<<( ostream & os,const Complex & c)
{
    os << c.real << "+" << c.imag << "i"; //以"a+bi"的形式输出
    return os;
}
   
istream & operator>>( istream & is,Complex & c)
{
    string s;
    is >> s; //将"a+bi"作为字符串读入, “a+bi”中间不能有空格
    int pos = s.find("+",0);
    string sTmp = s.substr(0,pos); //分离出代表实部的字符串
    c.real = atof(sTmp.c_str()); //atof库函数能将const char*指针指向的内容转换成 float
    sTmp = s.substr(pos+1, s.length()-pos-2); //分离出代表虚部的字符串
    c.imag = atof(sTmp.c_str());
    return is;
}

int main()
{
    Complex c;
    int n;
    cin >> c >> n;
    cout << c << "," << n;
    return 0;
}

运行结果可以如下:

13.2+133i 87    //输入
13.2+133i, 87   //输出

其他运算符重载

类型转换运算符”()”重载:

#include <iostream>
using namespace std;
class Complex
{
    double real,imag;
    public:
    Complex(double r=0,double i=0):real(r),imag(i) { };
    operator double () { return real; }
    //重载强制类型转换运算符 double
};
int main()
{
    Complex c(1.2,3.4);
    cout << (double)c << endl; //输出 1.2
    double n = 2 + c; //等价于 double n=2+c.operator double()
    cout << n; //输出 3.2
}

自增自减运算符”++,–”的重载:
自增运算符++、自减运算符–有前置/后置之分,为了区分所重载的是前置运算符还是后置运算符, C++规定:

  • 前置运算符作为一元运算符重载
  • 后置运算符作为二元运算符重载,多写一个没用的参数

前置运算符重载形式:

重载为成员函数:
T & operator++();   //不用写入参,当前对象的成员++
T & operator--();
重载为全局函数:
T1 & operator++(T2);
T1 & operator—(T2);

后置运算符重载形式:

重载为成员函数:
T operator++(int);  //多写一个入参,用于和前置重载区分
T operator--(int);
重载为全局函数:
T1 operator++(T2,int );
T1 operator—( T2,int);

调用示例:

int main()
{
    CDemo d(5);
    cout << (d++ ) << ","; //等价于 d.operator++(0);
    cout << d << ",";
    cout << (++d) << ","; //等价于 d.operator++();
    cout << d << endl;
    cout << (d-- ) << ","; //等价于 operator--(d,0);
    cout << d << ",";
    cout << (--d) << ","; //等价于 operator--(d);
    cout << d << endl;
    return 0;
}

class CDemo {
private :
    int n;
    public:
    CDemo(int i=0):n(i) { }
    CDemo & operator++(); //用于前置形式
    CDemo operator++( int ); //用于后置形式
    operator int ( ) { return n; }
    friend CDemo & operator--(CDemo & );
    friend CDemo operator--(CDemo & ,int);
};
CDemo & CDemo::operator++()
{ //前置 ++
    n ++;
    return * this;
} // ++s即为: s.operator++();

CDemo CDemo::operator++( int k )
{ //后置 ++
    CDemo tmp(*this); //记录修改前的对象
    n ++;
    return tmp; //返回修改前的对象
} // s++即为: s.operator++(0);
CDemo & operator--(CDemo & d)
{//前置--
    d.n--;
    return d;
} //--s即为: operator--(s);
CDemo operator--(CDemo & d,int)
{//后置--
    CDemo tmp(d);
    d.n --;
    return tmp;
} //s--即为: operator--(s, 0);

运算符重载注意事项

  • C++不允许定义新的运算符
  • 重载后运算符的含义应该符合日常习惯,即保留原运算符的使用风格
  • 运算符重载不改变运算符的优先级
  • 以下运算符不能被重载:“ .” “ .*” “ ::” “ ?:” “sizeof”
  • 重载运算符()、[]、->、=,运算符重载函数必须声明为
    类的成员函数

运算符重载的综合示例

实现一个可变长数组类型CArray,实现如下用例:

int main() { 
    CArray a; //开始里的数组是空的
    for( int i = 0;i < 5;++i)
        a.push_back(i); //要用动态分配的内存来存放数组元素,需要一个指针成员变量
    CArray a2,a3;
    a2 = a; //要重载“=”
    for( int i = 0; i < a.length(); ++i )
        cout << a2[i] << " " ;  //要重载[]
    a2 = a3; //a2是空的
    for( int i = 0; i < a2.length(); ++i )//a2.length()返回0
        cout << a2[i] << " ";
    cout << endl;
    a[3] = 100;
    CArray a4(a);   //要自己写拷贝构造函数
    for( int i = 0; i < a4.length(); ++i )
        cout << a4[i] << " ";
    return 0;
}

CArray类的设计:

class CArray {
    int size; //数组元素的个数
    int *ptr; //指向动态分配的数组
    public:
    CArray(int s = 0); //s代表数组元素的个数
    CArray(CArray & a);
    ~CArray();
    void push_back(int v); //用于在数组尾部添加一个元素v
    CArray & operator=( const CArray & a);
    //用于数组对象间的赋值
    int length() { return size; } //返回数组元素个数
    int & CArray::operator[](int i) //返回值为 int 不行!不支持 a[i] = 4
    {//用以支持根据下标访问数组元素,如n = a[i] 和a[i] = 4; 这样的语句
        return ptr[i];
    }
};

成员函数的实现:

CArray::CArray(int s):size(s)
{
    if( s == 0)
    ptr = NULL;
    else
    ptr = new int[s];
}
CArray::CArray(CArray & a) {
    if( !a.ptr) {
    ptr = NULL;
    size = 0;
    return;
    }
    ptr = new int[a.size];
    memcpy( ptr, a.ptr, sizeof(int ) * a.size);
    size = a.size;
}

CArray::~CArray()
{
    if( ptr) delete [] ptr;
}
CArray & CArray::operator=( const CArray & a)
{ //赋值号的作用是使“=”左边对象里存放的数组,大小和内容都和右边的对象一样
    if( ptr == a.ptr) //防止a=a这样的赋值导致出错
    return * this;
    if( a.ptr == NULL) { //如果a里面的数组是空的
    if( ptr ) delete [] ptr;
    ptr = NULL;
    size = 0;
    return * this;
    }
    if( size < a.size) {         //如果原有空间够大,就不用分配新的空间
        if(ptr)
        delete [] ptr;
        ptr = new int[a.size];
    }
    memcpy( ptr,a.ptr,sizeof(int)*a.size);
    size = a.size;
    return * this;
} // CArray & CArray::operator=( const CArray & a)

void CArray::push_back(int v)
{ //在数组尾部添加一个元素
    if( ptr) {
        int * tmpPtr = new int[size+1]; //重新分配空间
        memcpy(tmpPtr,ptr,sizeof(int)*size); //拷贝原数组
        内容
        delete [] ptr;
        ptr = tmpPtr;
    }
    else //数组本来是空的
    ptr = new int[1];
    ptr[size++] = v; //加入新的数组元素
}

0.概述

本章介绍C++语言和C语言相近的部分基础用法,包括

  • 引用: &
  • 常关键字: const
  • 动态内存分配: new delete
  • 函数内联: inline
  • 函数重载

引用和指针

引用的概念

下面的写法定义了一个引用,并将其初始化为引用某个变量。

类型名 & 引用名 = 某变量名;

某个变量的引用,等价于这个变量,相当于该变量起了一个别名。别名类似于操作系统的文件链接或快捷方式的概念,访问它变量本身的存储空间。

int n = 4;
int & r = n; // r引用了 n, r的类型是int &
r = 4;
cout << r; //输出 4
cout << n; //输出 4
n = 5;
cout << r; //输出5

注意:
1.定义引用时一定要将其初始化成引用某个变量。
2.初始化后,它就一直引用该变量,不会再引用别
的变量了。
3.引用只能引用变量,不能引用常量和表达式。

引用的示例

引用常用于函数传参和返回值
1.引用作为函数入参
C语言写一个swap函数,交换两个变量的值,要传指针而不能传值,因为直接传值实际修改的是函数局部作用域的一份拷贝。

void swap( int * a, int * b)
{
    int tmp;
    tmp = * a; * a = * b; * b = tmp;
}
int n1, n2;
swap(& n1,& n2) ; // n1,n2的值被交换

C++中,除了传指针,也可以传引用

void swap( int & a, int & b)
{
    int tmp;
    tmp = a; a = b; b = tmp;
}
int n1, n2;
swap(n1,n2) ; // n1,n2的值被交换

2.引用作为函数返回值

int n = 4;
int & SetValue() { return n; }
int main()
{
SetValue() = 40;
cout << n;
return 0;
} //输出: 40

引用和指针的区别

看上去引用和指针的功能相同,那区别在哪?
1.存储类型不同

  • 指针是一种变量,存储指向变量的地址值,通常占内存4字节(64位系统8字节)
  • 引用只是变量的别名,它本身不另外占存储空间,对其求大小(sizeof)就是变量本身的大小

指针是变量,因此可以为空(0x0),而引用是标签(别名),不可为空,先有变量才能有其引用。
2.作用方式不同

  • 指针作为函数入参本质上还是是值传递,只不过传递的是变量的地址值,函数局部拷贝的也是地址。
  • 引用作为函数入参,被调函数的形参作为局部变量在栈中开辟了内存空间,但存放的是主调函数的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。

对于函数传参,形参都是用地址值达成对实参的修改,但传指针是显式的,而传引用是编译器隐式处理的。
指针和引用在内存中的示意图:
image-20221208164349783

指针和引用的应用比较:
引用比指针使用起来形式上更为美观,使用引用指向的内容时可以之间用引用变量名,而不像指针一样要使用*;定义引用的时候也不用像指针一样使用&取址。
引用比指针更安全。由于不存在空引用,并且引用一旦被初始化为指向一个对象,就不能被改变为另一个对象的引用,因此引用很安全。对于指针来说,它可以随时指向别的对象,并且可以不被初始化,或为NULL,所以不安全。并且有可能产生野指针(即多个指针指向一块内存,free掉一个指针之后,别的指针就成了野指针)。

常量关键字const

定义常量

常量:不可被修改的内存单元
const int MAX_VAL = 23;
const string SCHOOL_NAME = “Peking University”;

定义常引用

定义引用时,前面加const关键字,即为“常引用”。不能通过常引用修改其引用的变量,但可直接修改变量的值,引用本身也不能改变

int n;
const int & r = n;
r = 5; //error
n = 4; //ok

const T & 和T & 是不同的数据类型!!!
T & 类型的引用或T类型的变量可以用来初始化const T & 类型的引用,const T 类型的常变量和const T & 类型的引用则不能用来初始化T &类型的引用,除非进行强制类型转换
一句话,常指针和常引用不能出现在“=”左边

定义常指针

常指针也叫常量指针。但指针不是常量,指向的也不是常量,只是限制了改写方式:不可通过常量指针修改其指向变量的值,但可直接修改变量的值,也可以改变常量指针的指向地址值。

int n,m;
const int * p = & n;
* p = 5; //编译出错
n = 4; //ok
p = &m; //ok, 常量指针的指向可以变化

不能把常量指针赋值给非常量指针,反过来可以

const int * p1; int * p2;
p1 = p2; //ok
p2 = p1; //error
p2 = (int * ) p1; //ok,强制类型转换

函数参数为常量指针时,可避免函数内部不小心改变参数指针所指地方的内容

void MyPrintf( const char * p )
{
strcpy( p,"this"); //编译出错
printf("%s",p); //ok
}

定义指针常量

定义:本质是一个不可修改指向地址的指针

int* const p;

定义指向常量的常指针

定义:指针指向的地址值不可修改,且该地址中的值也不可修改

const int* const p;

动态内存分配

动态内存分配是分配内存空间中堆(heap)的内存,实际上是程序内手动的内存分配与释放。并非堆栈中局部变量的入栈出栈,由操作系统控制的动态分配。

new分配内存

分配一个变量:

P = new T;

T是任意类型名, P是类型为T * 的指针。
动态分配出一片大小为 sizeof(T)字节的内存空间,并且将该内存空间的起始地址赋值给P:

int * pn = new int;
* pn = 5;

分配一个数组:

P = new T[N];

T :任意类型名
P :类型为T * 的指针
N :要分配的数组元素的个数,可以是整型表达式
动态分配出一片大小为 sizeof(T)*N字节的内存空间,并且将该内存空间的起始地址赋值给P

int * pn;
int i = 5;
pn = new int[i * 20];
pn[0] = 20;
pn[100] = 30; //编译没问题。运行时导致数组越界

delete释放内存

用“new”动态分配的内存空间用完后,一定要用“delete”运算符进行释放,否则操作系统无法再次使用这块内存,造成内存泄露
注意:不能对内存空间delete两次!

#delete 指针; //该指针必须指向new出来的空间
int * p = new int;
* p = 5;
delete p;
delete p; //导致异常, 一片空间不能被delete多次

用“delete”释放动态分配的数组,要加“[]”

#delete [] 指针; //该指针必须指向new出来的数组
int * p = new int[20];
p[0] = 1;
delete [] p;

内联函数

普通函数:编译出来的可执行程序加载到内存后,代码段只有一份函数的指令序列,函数的调用处就用一个类似jump的语句跳转到函数指令序列的入口地址
内联函数:函数的每个调用处都存在整个函数指令序列的拷贝
简单讲就是增加编译出来的代码占用空间,换取运行时频繁入栈出栈的时间开销
使用场景:简单函数体且多次调用可以定义为内联
函数调用是有时间开销的。如果函数本身只有几条语句,执行非常快,而且函数被反复执行很多次,相比之下调用函数所产生的这个开销就会显得比较大。为了减少函数调用的开销,引入了内联函数机制。编译器处理对内联函数的调用语句时,是将整个函数的代码插入到调用语句处,而不会产生调用函数的语句。

inline int Max(int a,int b)
{
if( a > b) return a;
return b;
}

函数重载

函数重载概念

重载不是重新载入,更贴切的含义是重复定义,因为重定义是种错误,重载可以理解为编译器能理解的“重定义”,因此能正常加载。
C++重载主要有:

  • 函数重载
  • 运算符重载

C++的类没有重载一说,本节讲函数重载
函数重载:一个或多个函数,名字相同,然而参数个数或参数类型不相同,这叫做函数的重载。

int Max(double f1,double f2) { }
int Max(int n1,int n2) { }
int Max(int n1,int n2,int n3) { }

Q1.重载有什么用?
C语言定义以上几个函数,不能用同名,但是其功能都是相同的,仅参数类型和值不同。如果用MaxDouble(),MaxInt2(),MaxInt3()过于麻烦。
因此函数重载使得函数命名变得简单。
Q2.编译器怎么知道调用的是哪个?
编译器根据调用语句的中的实参的个数和类型判断应该调用哪个函数,注意重载函数不会把入参自动类型转换,调用二义性会报错。

Max(3.4,2.5); //调用 (1)
Max(2,4); //调用 (2)
Max(1,2,3); //调用 (3)
Max(3,2.4); //error,二义性

Q3.函数仅返回值类型不同是不是重载?
不是,函数重载的区分在于入参。但是有个例外,返回const T和非const T的两个函数是是重载的,其他情况的入参相同,返回类型不同的函数,视为重定义。

缺省参数与可拓展性

C++函数支持缺省参数(默认参数值)。定义函数的时候可以让最右边的连续若干个参数有缺省值,那么调用函数的时候,若相应位置不写参数,参数就是缺省值。

void func( int x1, int x2 = 2, int x3 = 3)
{ }
func(10 ) ; //等效于 func(10,2,3)
func(10,8) ; //等效于 func(10,8,3)
func(10, , 8) ; //不行,只能最右边的连续若干个参数缺省

函数参数可缺省的目的在于提高程序的可扩充性。
如果某个写好的函数要添加新的参数,而原先那些调用该函数的语句,未必需要使用新增的参数,那么为了避免对原先那些函数调用语句的修改,就可以使用缺省参数。
在C语言中,如果函数新增一个入参,所有调用该函数的地方都要传入该入参值;C++支持缺省参数,只需要改函数定义即可,调用处不需要动。

0.概述

结构体(Struct):存放一组不同类型的数据的数据结构。
数组(Array):存放一组相同类型的数据的数据结构。
结构体和int,char,指针等基础数据类型一样,也是一种数据类型。格式定义如下:

struct 结构体名{
    成员类型1 成员名1;
    成员类型2 成员名2;
}实例1,实例2;

结构体的成员(Member)可以为任意类型,如int,char,指针,甚至结构体类型。
结构体可以配合结构体数组、普通指针、函数指针、以及自身嵌套的使用方法,实现复杂数据结构,以及面向对象的代码风格。

1.结构体声明

声明是告诉编译器某个数据结构的定义。一般在头文件对结构体、函数等类型声明。声明过程不分配内存。
一个结构体类型的声明:

struct stu{
    char *name;     //姓名
    int num;        //学号
    int age;        //年龄
    char group;     //所在小组
    float score;    //成绩
};

注意:
1.结构体声明类似于函数声明,是一个语句,末尾加;
2.结构体声明只声明了类型,不实例化变量,因此不分配内存。
3.结构体成员的变量只在实例结构体才分配内存。

2.结构体实例

2.1基础方法

结构体变量是结构体类型的实例,实例化就是在内存分配一个结构体类型的变量空间。
方法一:先声明结构体类型stu,再实例变量stu1,stu2。
该方法结构体类型声明和实例化分离。声明一次,到处实例化。注意实例变量要带struct关键字。

struct stu stu1, stu2;

方法二:声明的时候也实例变量stu1,stu2。

struct stu{
    char *name;     //姓名
    int num;        //学号
    int age;        //年龄
    char group;     //所在小组
    float score;    //成绩
} stu1, stu2;

方法三:不声明接头体名,直接实例变量。
适用于只需要 stu1、stu2两个变量,后面不需要再使用结构体名定义其他变量的情况

struct{             //没有写stu
    char *name;     //姓名
    int num;        //学号
    int age;        //年龄
    char group;     //所在小组
    float score;    //成绩
} stu1, stu2;

2.2重定义方法

结构体类型通常配合typedef重定义后声明。
声明一次,到处实例化。不需要带struct关键字。

    typedef struct stu{ //stu可省略
        char *name;     //姓名
        int num;        //学号
        int age;        //年龄
        char group;     //所在小组
        float score;    //成绩
    }stu_t, *stu_p;    //声明stu_t为stu类型,stu_p为stu类型的指针

实例变量:

stu_t stu1,stu2;    //实例两个stu结构体变量
stu_p *stu1_p, *stu2_p; //实例两个指向stu结构体指针
stu1_p = &stu1; //指向实例stu1
stu2_p = &stu2; //指向实例stu2

3.结构体初始化

初始化=赋初始值。
结构体的实例只分配了内存,其成员的值要手动赋值后才能确定。否则直接拿来用会得到不确定的值(取决于分配到的内存原本的值)。
以重定义的结构体类型stu_t为例,实例变量时顺便初始化所有成员为0:

stu_t stu1,stu2 = {0};

有的编译器可能要求这种写法:

stu_t stu1,stu2 = {{0}};

如果各成员有默认初始值,初始化如下:

stu_t stu1, stu2 = { "Tom", 9527, 18, 'A', 136.5 };

4.结构体的赋值

结构体赋值是对结构体变量内的成员赋值。
两种方式访问成员:

结构体变量.成员名;
stu1.name = 'Tom';
结构体指针->成员名
stu1_p->name = 'Tom';

这两种方法的选择取决于使用情况。如果结构体作为参数在函数之间频繁传递和赋值,建议使用传指针,而不是传结构体变量,这样减少函数为结构体频繁分配局部内存,但要注意结构体已被释放,形成空指针的判断。
注意是对实例的成员赋值(已分配内存),而不能对结构体类型的成员赋值(只是个标签)。例如如下操作是错误的:

stu_t.name = "Tom"  //错,stu_t是类型
stu1_p = &stu_t     //错,stu_t没地址

可以对结构体类型进行sizeof操作,而不需要分配内存。

sizeof(stu_t);      //获取结构体(将)占用的内存空间

5.结构体的内存分配

理论上结构体的内存占用是成员占用的和。各成员在内存中连续存储的,和数组非常类似,例如结构体变量 stu1、stu2的内存分布如下,共占用 4+4+4+1+4=17字节。
1
但实际上,编译器会遵循内存对齐规则。实际内存占用大于各成员占用的和。如下图,stu1、stu2 其实占用了 17+3=20 字节
2

5.1内存对齐概述

1.CPU怎么访问内存中的数据最高效?
答:用最少的访问次数,获取该数据所在的内存空间的值。
2.怎么做到对某类型数据的最少访问?
答:编译器设置数据的存放地址的单位为数据占用空间的长度,CPU以数据长度为单位查询偏移地址,找到数据空间首地址后,根据数据类型取出其占用空间大小的数据。
如int数据,就存放在以4字节为单位的偏移地址,如0,4,8…,CPU取数据就按0,4,8…的地址查询,找到该数据地址后取4字节。这样做到一次性访问获取int数据。如果CPU按单字节访问int,就要查询4次,如int首字节地址为0x00001024, CPU要分4次查询0x00001024~0x00001027才能得到一个int。
3.对于结构体,包含多种数据类型,怎么对齐?
答:各成员按各自的类型对齐,即对于成员来说不存在结构体的概念,它认为它就是基本的数据类型int、char、指针等。

5.2结构体与内存对齐

结构体是不同类型数据的集合,因此内存对齐问题就特别突出。一个例子:

#include "stdio.h"
typedef struct {
    int a;
    double b;
    char c;
}A; 

typedef struct {
    int a;
    char b;
    double c;
}B;

int main()
{
    printf("sizeof A: %d, sizeof B: %d\n", sizeof(A), sizeof(B));
}

A和B的内存占用:A=24字节,B=16字节。
3
结构体内存对齐的计算规则:
1.默认首地址已对齐(或认为是0地址)
2.各成员按自己的类型对齐
3.整个结构体分配的空间是期中最大成员占用空间的整数倍
对于A:
int a占用4字节,地址byte[03]
double b占8字节,起始地址必须是8的倍数,占用byte[7
15]
char c占1字节,因此占byte[16]
目前共占用17字节。编译器会按照规则3,将byte[17~13]也分配给结构体,因此最终结构体占用38=24字节。
对于B, int a和char b加起来都不够8字节,double c再占用8字节,共占用2
8=16字节。
因此结构体的内存分配=各成员按类型对齐+总空间是最大成员空间的倍数
注意,结构体不仅成员间要对齐,最后一个成员后面的空余空间可能也分配给结构体。
查看下面的测试程序:

typedef struct {
    int a;
    double b;
    char c;
    char d;         //d作为成员
}C;

typedef struct {
    int a;
    double b;
    char c;
    struct {        //d作为嵌套结构体的成员
        char d; 
    };
}D;

printf("sizeof C: %d, sizeof D: %d\n", sizeof(C), sizeof(D));

C和D的内存占用:
4
新增的char d作为C的成员被分配在第三个8byte区域的第二个字节(byte[17]),嵌套的结构体并不从第四个8byte开始分配,它占用空间还是byte[17]。可见编译器对结构体内存分配不区分成员类型,只根据成员大小来处理。

6.联合、位域、枚举

这几种数据结构体和结构体相关联,通常混合使用。

6.1联合

联合(Union)也称共用体,和结构体的区别:
结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
声明格式:

union 共用体名{
    成员列表
};

共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
联合的一个示例:

#include <stdio.h>
union data{
    int n;
    char ch;
    short m;
};
int main(){
    union data a;
    printf("%d, %d\n", sizeof(a), sizeof(union data) );
    a.n = 0x40;
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
    a.ch = '9';
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
    a.m = 0x2059;
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
    a.n = 0x3E25AD54;
    printf("%X, %c, %hX\n", a.n, a.ch, a.m);
    return 0;
}

输出:

4, 4
40, @, 40
39, 9, 39
2059, Y, 2059
3E25AD54, T, AD54

在内存中数据分布如下(以大端,低字节存高位为例)
5
可见数据会相互覆盖,联合可以理解为分时复用的结构体,其空间占用定长,为最大的成员长度,在不同时间,值的含义不同。

6.2位域

有的结构体成员在存储时并不占用一个完整的字节,只需要按二进制位为单位分配空间即可。可以指定该成员所占用的二进制位数(Bit),这就是位域。

#include "stdio.h"
struct {
        unsigned char a;    //a占完整的8bit
        unsigned char b: 2; //b占2bit
        unsigned char c: 6; //C占6bit
}bs;

int main()
{
    printf("sizeof bs: %d\n", sizeof(bs));
}

输出2字节,可见b和c刚好拼成一个unsigned char(8 bit):
6
位域将结构体成员占用的空间从基本数据类型为单位,变成了以二进制位为单位,是更精细的结构体内存分配。
位域不能超过对应基本类型的二进制位数。

6.3枚举

枚举可以理解为计数宏的结构体。

#include <stdio.h>
int main(){
    enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day;
    scanf("%d", &day);
    switch(day){
        case Mon: puts("Monday"); break;
        case Tues: puts("Tuesday"); break;
        case Wed: puts("Wednesday"); break;
        case Thurs: puts("Thursday"); break;
        case Fri: puts("Friday"); break;
        case Sat: puts("Saturday"); break;
        case Sun: puts("Sunday"); break;
        default: puts("Error!");
    }
    return 0;
}

枚举值默认从0开始,往后逐个加 1(递增);如果第一个成员赋值,从那个成员值往后递增。也就是说,week中的 Mon、Tues …… Sun 对应的值分别为 1、2… 7。
宏在编译的预处理阶段将名字替换成对应的值,而枚举在编译阶段将名字替换成对应的值。在编译过程中,Mon、Tues、Wed 名字都被替换成了对应的数字。这意味着Mon、Tues、Wed 等都不是变量,不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是被编译到指令里面,放到代码区,所以不能用&取得它们的地址。这就是枚举的本质。
枚举类型实例的内存占用通常=int类型占用=4字节。

7.结构体常见用法

7.1结构体数组

结构体数组是将多个同类型结构体按数组的方式存储,其成员访问方式为:先访问数组元素,再访问结构体成员。
结构体数组本质还是数组,但数组成员是结构体,结构体内可以包含各种类型的成员。
一个Linux NandFlash驱动的结构体数组如下:

    static struct mtd_partition s3c_nand_parts[] = {
    [0] = {
        .name   = "bootloader",
        .size   = 0x00040000,
        .offset    = 0,
    },
    [1] = {
        .name   = "params",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00020000,
    },
    [2] = {
        .name   = "kernel",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00200000,
    },
    [3] = {
        .name   = "root",
        .offset = MTDPART_OFS_APPEND,
        .size   = MTDPART_SIZ_FULL,
    }
};

该数组名为s3c_nand_parts,成员为mtd_partition结构体,包含分区名,分区大小和分区偏移地址。每个结构体成员分别初始化赋值。注意结构体数组实例没写结构体名,只有数组下标[],结构体成员没写结构体名,只有.符号。这是Linux kernel常见的精简写法。
访问一个结构体数组的成员:

s3c_nand_parts[0].name = "bootloader_2"

结构体数组的内存占用=数组成员数*单个结构体内存占用。

7.2结构体指针

结构体指针本质是指针变量,其值是结构体的地址。
前面结构体重定义一节已经定义和初始化过结构体指针,需要注意的是,结构体指针的初始化值来源于结构体实例,结构体类型名只是标签,不代表结构体地址,注意和”数组名=数组地址“区分。

struct stu{         
    char *name;     //姓名
    int num;        //学号
    int age;        //年龄
    char group;     //所在小组
    float score;    //成绩
} stu1, stu2;

struct stu *stu_p = stu;    //错,stu只是符号,不占内存
struct stu *stu_p = &stu1;  //对,stu1是结构体实例,占内存  //对,stu1是stu实例变量,有内存占用

结构体指针的常见用途:malloc分配结构体空间

stu_p =(stu_t *)malloc(sizeof(stu_t));  //分配结构体空间,返回地址给结构体指针

结构体指针最重要的用途:函数传参
结构体变量作为函数参数时传递的是整个结构体内存空间,也就是所有成员空间,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。

7.2结构体嵌套

结构体嵌套是结构体的成员也是结构体。有两种情况:
1.成员是其他类型的结构体
2.成员是同类型的结构体

7.2.1.嵌套其他类型

在协议开发中,一个命令的数据可以用结构体来表达,在命令的内部又分为很多个数据域,每个数据域又用结构体来表达,因此需要结构体嵌套。对于同一块数据,根据命令的不同,解析为不同的结构体,因此存在多类命令公用一块数据域的情况,因此需要联合(Union)。以NVMe协议为例,结构体嵌套和联合一起使用的例子:

typedef struct  //nvme命令结构体
{
     union      //命令中dword10空间的联合
    {
        u32 command_dw10;
        struct
        {
            u32 cntid:16;   //16 bit位域
            u32 resv1:8;    //8 bit位域
            u32 cns:8;      //8 bit位域
        }identify;          //当命令为identify时
        struct
        {
            u32 save:1;
            u32 resv1:20;
            u32 select:3;
            u32 feature_identifier:8;
        }get_features;      //当命令为get_featuresy时
        struct
        {
            u32 queue_size:16;
            u32 queue_identifier:16;
        }io_queue_create_delete_dw10;
        ...
    };
    
    union   //命令中dword11空间的联合
    {
        u32 command_dw11;
        struct
        {
            u32 completion:16;
            u32 submission:16;
        }number_of_queues;
        struct
        {
            u32 interrupt_vector:16;
            u32 resv1:14;
            u32 interrupt_enabled:1;
            u32 physically_contiguous:1;
        }create_io_completion_queue_dw11;
        ...
    };
    
}command_t, *command_p;

该例子结合了结构体、联合、位域。对每个nvme命令,多个联合并存在结构体command_t实例里,每个联合长度为一个dword(4字节),分别表示dword0~15中的一个。对于dword内部,根据解析到命令的不同,作为不同含义处理,如解析为identify就按identify的结构体读写成员,如解析为get_features则按get_features的结构体读写成员。在结构体内部,用位域更精细控制这个dword内各bit的含义。

7.2.2嵌套自身类型

结构体嵌套自身类型的典型应用:链表数据结构体

typedef struct ListNode {
    DataType data;          // 节点数据 
    struct ListNode *next;  // 指向下一个结点的指针 
} ListNode_t;

这个结构体有两个成员:DataType类型的数据,和指向 struct ListNode类型(=ListNode_t类型)的实例的指针。有多个ListNode_t类型的结构体被实例化且依次指向后续节点后,可以依次node1->next->next…->data访问链表中的节点数据。
注意:结构体体能嵌套自身类型的指针,而不能嵌套自身类型的实例。因为指针分配内存是定长(通常4字节),而循环嵌套结构体变量是无穷的。以下写法是错的

typedef struct ListNode {
    DataType data;           
    struct ListNode next;  // 错,嵌套的是实例 
} ListNode_t;

8.结构体高级用法:面向对象

8.1函数指针

程序中定义的函数,在编译时会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的入口地址。函数名表示的就是这个地址的值。可以定义一个指针变量来存放函数的入口地址,这个指针变量就叫作函数指针变量,简称函数指针。
这段话什么意思?
1.函数名=函数入口地址
2.可以用指针变量的值取代函数名,函数的调用和该指针变量的调用等价
3.可以把这个指针变量当参数传递给别的函数,也可以把这个指针变量作为结构体的成员,总之,一切指针能做的,函数指针都能做。
函数指针的定义:

返回值 函数入口地址(入参1的类型,入参2的类型,...)
int (*p)(int, int);     //p为函数指针,*p为入口地址

定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。p 的类型为 int(*)(int,int)
函数指针的初始化:

int Func(int x);   /*声明一个函数,包含入参名*/
int (*p) (int);  /*声明一个函数指针,只有入参类型*/
p = Func;         /*将Func函数的入口地址赋给指针变量p*/

函数指针作为结构体成员的调用如下

struct{
    int (* func)(int);
}stu1, *stu1_p; //分配两个结构体实例:stu1结构体和指针stu1_p

stu1.func(10);  //通过结构体调用函数指针
stu1_p->func(10);   //通过结构体指针调用函数指针

8.2回调函数

回调函数(Callback)就是一个通过函数指针调用的函数。把函数指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由函数的实现方直接调用,而是在特定的事件或条件发生时,由把它当参数的那个函数调用的,用于对该事件或条件进行响应。
回调的两个特点:
1.函数=参数。函数A指针当参数传递B
2.异步。特定时间发生时,B才调用A指针指向的函数

8.3结构体与面向对象

当函数指针作为结构体的成员,可以通过结构体实例调用成员函数,此时可以实现类似其他语言中“类”或“接口”的概念:
结构体声明=类声明=接口声明
结构体的函数指针成员=类方法=接口函数
结构体实例调用函数指针成员=类实例调用方法=接口的实现
这种设计思想在Linux内核和驱动框架中很常用。以字符设备驱动为例:
字符设备驱动顶层框架将所有字符操作函数作为接口在结构体file_operations中定义,在底层具体的设备驱动中实现file_operations的方法。底层驱动实例化file_operations结构体(分配内存),将各种操作的具体实现函数赋值给接口定义的函数,然后上报(注册)该file_operations实例给顶层驱动框架,顶层驱动框架接收到应用层的系统调用请求时,回调已注册的file_operations实例的函数。
上层驱动框架定义的字符文件操作接口如下,这些open、read、write作为文件操作的方法供应用层调用。

 struct file_operations {   //字符文件操作的接口定义
  struct module *owner;   //结构体指针
  ssize_t(*read) (struct file *, char __user *, size_t, loff_t *);      //函数指针read
  ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *);    //函数指针write
  int (*open) (struct inode *, struct file *);    //函数指针open
  int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);   //函数指针ioctl
  ...
};

而这些方法具体实现(内部做什么)是由底层驱动代码实现:

static int my_drv_open(struct inode *inode, struct file *file)
{
    //硬件寄存器操作A...
    return 0;
}

static ssize_t my_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
    //硬件寄存器操作B...
    return 0;
}

那么这些实现怎么关联接口:在底层驱动分配结构体实例,初始化函数指针为实现函数

static struct file_operations my_drv_fops = {
    .owner  =   THIS_MODULE,   
    .open   =   my_drv_open,   //open接口由my_drv_open函数实现  
    .write    =    my_drv_write,     //write接口由my_drv_write函数实现  
};

关联完了,上层驱动怎么调用:结构体指针传参+回调
my_drv_fops是file_operations的实例,上报给上层驱动,上层驱动有它自己的字符设备结构体cdev,取出my_drv_fops实例的.结构体指针fops的值,赋给cdev实例内的ops指针。之后它就能用ops调用my_drv_open、my_drv_write函数。

//注册结构体
cdev->owner = fops->owner;
cdev->ops = fops;
//回调.open方法
cdev->ops->open(inode,file);

由于my_drv_write是值,open才是结构体成员,因此调用的时候看上去调用的是open函数,本质上执行的还是my_drv_write的流程。函数指针的回调能将接口名暴露,方法名隐藏。因此底层实现的函数名和上层驱动的调用函数名不相关,上层永远都可以用.open .read .write这些接口调用底层驱动,而底层函数可以随便改名(不能改入参出参类型,否则函数指针类型变了),这种特性都是函数指针决定的。
对于应用程序,是调用上层驱动提供的系统调用接口,还是如.open .read .write等接口。Linux驱动将设备抽象成了文件,驱动程序实现了文件的各种方法,所以对应用程序,打开文件=打开设备,调用文件对应的接口=调用设备驱动提供的接口。

fd = open("/dev/xyz", O_RDWR);  //打开文件(设备)
read(fd, &val, 1);  //读fd文件(设备)的值到val变量

除了C语言,在golang中也有类似的结构体+函数指针实现的面向对象方法。
NOTE:关于结构体中的函数指针写法易错点:
不包含变量名,只能使用基本类型

   #一个结构体
   typedef stru{    //定义结构体名
        int a;
        char b;
   }stru_t, *stru_p; //重定义结构体变量和指针
    
    #普通函数声明
    int func(int a, stru_p p); //参数写类型且写值,可以使用typedef后的结构体指针类型
    
    #函数指针声明
    int (*func)(int, struct stru *);    //参数只写类型不写参数,只能使用C基本类型,不能使用typedef后的类型,只能写struct stru *类型

函数指针声明使用typedef后的类型名,编译器不认识,产生syntax error。

查找包含指定内容的文件

grep -r 字符串 目录

示例:查找当前目录的包含“stream”内容的文件:

1
grep -r "stream" ./

zip/unzip

1
2
zip xxx.zip -r <DIR>  
unzip xxx.zip -d <DIR>

背景

配置x’shell的默认字体以及一些操作设置

会话设置

设置主机
1
设置主机账号密码
2
设置xshell字体
3

操作设置

设置Ctrl+c,Ctrl+v复制粘贴
工具->选项->编辑按键
4
新建按键,按ctrl+c
5
设置功能为复制
6
同理设置ctrl+v为粘贴
注意,原shell的ctrl+c终止程序,被替换成了shift+ctrl+c

本文记录在高通开发平台HDK845上编译Android系统镜像的过程

一、 搭建Shadowsocks+Privoxy代理

1.1为什么需要搭代理

下载Android源码需要访问国外代码源,直接访问会被GFW阻挡,代理服务器(VPS)是未被GFW阻挡的国外服务器,通过代理服务器跳转至目标服务器访问国外代码源。

1.2 shadowsocks+privoxy代理架构

使用shadowssocks+privoxy搭建客户端代理,如下图客户端进程发送请求(http/https/git)到privoxy,privoxy将请求转化为socks5请求,发送给shadowsocks客户端,shadowsocks处理socks5请求,将其发送到远端VPS上运行的socks5服务端(shadowsocks server),VPS再将请求转发给目标服务器。

image001

1.3 shadowsocks+privoxy代理搭建

1.3.1 shadowsocks

HOST系统:ubuntu 14.04 LTS
安装shadowsock

1
2
apt-get -y install python-pip 
pip install shadowsocks

配置shadowsocks client

1
2
3
4
5
6
7
8
9
10
11
12
gedit /etc/ss.json 
输入以下内容:
{
"server":"176.122.xxx.xx",
"server_port":8080,
"local_address":"127.0.0.1",
"local_port":1080,
"password":"xxxxx",
"timeout":100,
"method":"aes-256-cfb"
}

运行shadowsocks客户端

1
2
3
sslocal -c /etc/ss.json > ss.log 2>&1 &  
查看服务是否起来:
ps -ef | grep sslocal

若开机启动可写入/etc/rc.local

1.3.2 privoxy

下载privoxy稳定版本

1
2
3
wget http://www.privoxy.org/sf-download-mirror/Sources/3.0.26%20%28stable%29/privoxy-3.0.26-stable-src.tar.gz 
tar -zxvf privoxy-3.0.26-stable-src.tar.gz
cd privoxy-3.0.26-stable

privoxy服务需要新建privoxy用户,并添加到privoxy用户组来运行

1
2
3
useradd privoxy 
groupadd -g 888 privoxy
gpasswd -a privoxy privoxy

查看privoxy用户信息

1
id privoxy 

安装provoxy

1
2
3
4
apt-get -y install autoconf 
autoheader && autoconf
./configure
make && make install

设置privoxy监听http/https/git的端口,和privoxy面向socks5的端口

1
2
3
4
gedit /usr/local/etc/privoxy/config 
下面两行取消注释
listen-address 127.0.0.1:8118
forward-socks5t / 127.0.0.1:1080

启动privoxy

1
2
privoxy --user privoxy /usr/local/etc/privoxy/config 
ps -ef | grep sslocal

若开机启动可写入/etc/rc.local

1.3.3 设置代理环境变量

http/https/ftp请求的代理端口设置为privoxy的监听端口

1
2
3
4
gedit /etc/profile 
export http_proxy="http://127.0.0.1:8118"
export https_proxy="http://127.0.0.1:8118"
export ftp_proxy="http://127.0.0.1:8118"

生效并测试, curl返回大堆json字符串

1
2
source /etc/profile 
curl http://www.google.com

系统的http(s)等请求的代理配置完成

1.3.4 设置git代理

安装并配置git

1
2
3
4
5
apt-get install git 
git config --global user.email "yourname@xxx.com"
git config --global user.name "yourname"
git config --global http.proxy http://127.0.0.1:8118
git config --global https.proxy http://127.0.0.1:8118

设置git使用代理

1
2
3
4
5
apt-get install connect-proxy 
mkdir ~/bin
echo "connect-proxy -S 127.0.0.1:1080 \"\$@\"" > ~/bin/socks5proxywrapper
chmod 755 ~/bin/socks5proxywrapper
git config --global core.gitproxy `echo $HOME`/bin/socks5proxywrapper

二、下载编译Android源码

2.1 交叉编译的概念

- 1 本地编译:在当前编译平台下,编译出来的程序只能运行在当前平台。常见的应用软件开发的编译都属于本地编译。
- 2 交叉编译:在当前编译平台下,编译出来的程序能运行在另一种体系结构不同的目标平台上,但是编译平台本身却不能运行该程序。
- 3 交叉编译工具链:编译过程包括了预处理、编译、汇编、链接等过程。每个子过程都是单独的工具来实现。交叉编译链是为了编译跨平台体系结构的程序代码而形成的由多个子工具构成的一套完整的工具集。
image003
如上图,交叉编译工具链中最主要的部分包含编译器(如gcc),汇编器(如as),连接器(如ld)。通常as和ld及objcopy等其他工具由GNU打包成了binutils(binary utilitys)工具,再加上编译器组成整个工具链。
其中编译器命名规则为:

1
2
3
4
5
6
7
arch-core-kernel-system-compiler 

arch:目标平台架构,如arm, x86_64
core: 目标平台的CPU Core,如Cortex A8
kernel: 目标平台所运行的OS,如Linux,Android
systen:交叉编译链所选择的库函数和目标系统的规范,如gnu,gnueabi等
compiler: 编译器名,如gcc, g++,clang,clang++

- 4 交叉编译架构:
HOST OS 通常为Linux,包含自身的kernel、glibc基础库和Target程序的依赖库。Toolchain包含C/C++及其他语言编译器和汇编、链接器等组件。Toolchain依赖于HOST的glibc基础库。Target binary是编译出的目标镜像/程序,编译过程依赖于Toolchain及HOST的build essential libs。
image005

2.2 高通Android平台编译概念

高通平台HDK845推荐的编译环境如下:

HOST Toolchain Source code repository build out Android version
Ubuntu14.04 LTS Clang/LLVM CAF support Android 9 Pie

高通平台HDK845推荐的编译流程如下:
image006

Clang/LLVM编译器介绍
clangLLVM
CAF和AOSP的介绍

1
2
3
4
5
6
7
8
9
10
11
12
CAF is Code Aurora repository. It's the place where Qualcomm releases source code for their phone processors.  
It's directly supported by Qualcomm and it's generally a more optimized branch for Snapdragon phones.
Actually, there are two main baselines for support of Qualcomm devices:
- 1. CodeAurora (CAF) - These are Qualcomm's reference sources for their platform.
This is what they provide to OEMs, and what nearly all OEMs base their software off of.
As a result - nearly all non-Nexus devices are running kernels/display HALs/etc. that are derived from a CAF baseline.
- 2. Google's software baseline(AOSP) - Usually when Google starts working on a new Android version, they'll fork from CAF at the beginning.
Very often Google will be adding "new" features specific to the new Android version, while Qualcomm will continue with performance enhancements and bugfixes against the "old" baseline.
- 3. So when a new Android revision comes out, you have two baselines: CAF which is usually "ahead" in performance but "behind" in features, while AOSP is “behind” in performance (relatively) but “ahead” in features.
Nowadays, developers are directly compiling the builds from CAF source code which is really difficult as this is what Google does initially before upgrading to a new version,
and then they add features and the source by the time gets ‘compilable’, it is easier to compile the one on Google Sources than the one which is there on CAF.
CAF can be considered as Vanilla version of a Vanilla version of Android.

2.3 高通Android平台编译流程

- 1 安装jdk,用于编译Android源码中的java代码:

1
2
3
4
5
add-apt-repository ppa:openjdk-r/ppa 
apt-get update
apt-get -y install openjdk-8-jdk
update-alternatives --config java
java -version

- 2 安装HOST(ubuntu14.04)的build essentials,编译过程依赖这些工具和库

1
apt-get -y install git-core gnupg flex bison gperf build-essential zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip libssl-dev libc6:i386 libstdc++6:i386

- 3 安装repo,用于下载android源码

1
2
3
4
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo 
chmod +x ~/bin/repo
export PATH=~/bin:$PATH
repo --help

- 4 解压开发板厂商的BSP,其中包含源码下载的脚本、补丁包等

1
2
3
unzip Open-Q_845_Android-P_v2.1.zip 
cd Open-Q_845_Android-P_v2.1/Source_Package
chmod +x getSource_and_build.sh

- 5 用脚本从CAF源下载代码,打补丁后编译

1
./getSource_and_build.sh 

./getSource_and_build.sh内容如下

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
UNDER='\e[4m' 
RED='\e[31;1m'
GREEN='\e[32;1m'
YELLOW='\e[33;1m'
BLUE='\e[34;1m'
MAGENTA='\e[35;1m'
CYAN='\e[36;1m'
WHITE='\e[37;1m'
ENDCOLOR='\e[0m'
ITCVER="P_v2.1"
WORKDIR=`pwd`
CAFTAG="LA.UM.7.3.r1-06700-sdm845.0"
BUILDROOT="${WORKDIR}/SDA845_Open-Q_845_Android-${ITCVER}"
PATCH_DIR="${WORKDIR}/patches"
DB_PRODUCT_STRING="Open-Q 845 HDK Development Kit"

function download_CAF_CODE() {
# Do repo sanity test
if [ $? -eq 0 ]
then
echo "Downloading code please wait.."
repo init -q -u git://codeaurora.org/platform/manifest.git -b release -m ${CAFTAG}.xml
repo sync -q -c -j 4 --no-tags --no-clone-bundle
if [ $? -eq 0 ]
then
echo -e "$GREEN Downloading done..$ENDCOLOR"
else
echo -e "$RED!!!Error Downloading code!!!$ENDCOLOR"
fi
else
echo "repo tool problem, make sure you have setup your build environment"
echo "1) http://source.android.com/source/initializing.html"
echo "2) http://source.android.com/source/downloading.html (Installing Repo Section Only)"
exit -1
fi
}

# Function to check result for failures
check_result() {
if [ $? -ne 0 ]
then
echo
echo -e "$RED FAIL: Current working dir:$(pwd) $ENDCOLOR"
echo
exit 1
else
echo -e "$GREEN DONE! $ENDCOLOR"
fi
}

# Function to autoapply patches to CAF code
apply_android_patches()
{
echo "Applying patches ..."
if [ ! -e $PATCH_DIR ]
then
echo -e "$RED $PATCH_DIR : Not Found $ENDCOLOR"
return
fi
cd $PATCH_DIR
patch_root_dir="$PATCH_DIR"
android_patch_list=$(find . -type f -name "*.patch" | sort) &&
for android_patch in $android_patch_list; do
android_project=$(dirname $android_patch)
echo -e "$YELLOW applying patches on $android_project ... $ENDCOLOR"
cd $BUILDROOT/$android_project
if [ $? -ne 0 ]; then
echo -e "$RED $android_project does not exist in BUILDROOT:$BUILDROOT $ENDCOLOR"
exit 1
fi
git am --3way $patch_root_dir/$android_patch
check_result
done
}

# Function to check whether host utilities exists
check_program() {
for cmd in "$@"
do
which ${cmd} > /dev/null 2>&1
if [ $? -ne 0 ]
then
echo
echo -e "$RED Cannot find command \"${cmd}\" $ENDCOLOR"
echo
exit 1
fi
done
}

#Main Script starts here
#Note: Check necessary program for installation
echo
echo -e "$CYAN Product : $DB_PRODUCT_STRING $ENDCOLOR"
echo -e "$MAGENTA Intrinsyc Release Version : $ITCVER $ENDCOLOR"
echo -e "$MAGENTA WorkDir : $WORKDIR $ENDCOLOR"
echo -e "$MAGENTA Build Root : $BUILDROOT $ENDCOLOR"
echo -e "$MAGENTA Patch Dir : $PATCH_DIR $ENDCOLOR"
echo -e "$MAGENTA CodeAurora TAG : $CAFTAG $ENDCOLOR"
echo -n "Checking necessary program for installation......"
echo
check_program tar repo git patch
if [ -e $BUILDROOT ]
then
cd $BUILDROOT
else
mkdir $BUILDROOT
cd $BUILDROOT
fi

#1 Download code
download_CAF_CODE
cd $BUILDROOT

#2 Apply Open-Q 845 HDK Development Kit Patches
apply_android_patches

#3 Extract the proprietary objs
cd $BUILDROOT
echo -e "$YELLOW Extracting proprietary binary package to $BUILDROOT ... $ENDCOLOR"
tar -xzvf ../proprietary.tar.gz -C vendor/qcom/

#4 Build
echo -e "$YELLOW Building Source code from $BUILDROOT ... $ENDCOLOR"
if [[ -z "${BUILD_NUMBER}" ]]; then export BUILD_NUMBER=$(date +%m%d%H%M); fi
. build/envsetup.sh
lunch sdm845-${BV:="userdebug"}
ITC_ID=Open-Q_845_${ITCVER} make -j $(nproc) $@

编译后生成bootloader和系统等镜像:
SDA845_Open-Q_845_Android-P_v2.1/out/target/product/sdm845/xxx.img
后续重新编译只需要注释掉./getSource_and_build.sh步骤#1 #2 #3,保留#4 Build

二、 烧写Android镜像

3.1 烧写、调试、打印的工具

开发板通过micro USB和type-C USB连接到主机
type-C: 用于开发板接收adb/fastboot
micro USB: 用于HOST接收开发板的输出打印
连接如下:

image007

HOST端用到的工具:
fastboot: 用于烧写Android镜像到开发板
adb(Android Debug Bridge): 用于调试Android系统
secureCRT: 用于查看开发板串口打印
- 1 首先配置fastboot和adb到系统环境变量,windows环境下win + R输入cmd配置PATH变量

1
2
set PATH=%PATH%;d:\platform-tools\adb.exe
set PATH=%PATH%;d:\platform-tools\fastboot.exe

确认adb和fastboot加到了PATH环境变量

1
echo %PATH%

- 2 查看开发板对应的com口,secureCRT新建会话,设置serial,设置com口和波特率115200

image009

3.2 烧写镜像

- 1 首先使开发版进入fastboot模式,连接micro USB,电源选项拨到DC电源, 上电后长按vol-, 然后连接type-C,串口打印出现Fastboot: Processing commands则进入fastboot。

image011
- 2 win + R打开cmd,用fastboot烧写编译出来的镜像

1
2
3
4
5
6
7
fastboot flash system system.img 
fastboot flash persist persist.img
fastboot flash boot boot.img
fastboot flash dtbo dtbo.img
fastboot flash vbmeta vbmeta.img
fastboot flash vendor vendor.img
fastboot reboot

可写入flash.bat脚本,放到系统镜像同一目录下运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@echo off 

@echo Reboot bootloader...
adb reboot bootloader

@echo Flashing device...
fastboot flash system system.img
fastboot flash persist persist.img
fastboot flash boot boot.img
fastboot flash dtbo dtbo.img
fastboot flash vbmeta vbmeta.img
fastboot flash vendor vendor.img

@echo Flashing finish, rebooting system...
fastboot reboot

image012
完成后系统重启进入Android桌面。

image014