C++如何定义好一个类

204 阅读4分钟

1. 前言

我们在编写程序的时候时常会考虑我的类是否设计的合理,是否能达到预期的功能效果,是否具备良好的扩展性、易读性等等。编写出一个类并不难,但是要设计好一个类就需要不断的进行思考,每一个程序都有输入输出,我们在设计时首先要明白我们的输入输出是啥,因为我们所编写的程序是对用户进行的输入进行一系列的处理,最终给用户返回一个可靠的结果,所以常说程序等于数据结构加上算法。C++其实可以看做两部分:1、C++的语言部分 2、C++标准库 这两部分都是比较难啃的骨头,但是只要感兴趣,多写多练熟能生巧,慢慢的C++会变成手中的利刃。

2. 设计类时我们应时刻注意的事项

2.1 设计类时对头文件进行防卫式声明

C++ 程序代码基本形式:.h(头文件)我们一般在其中进行声明,.cpp中做函数具体的实现。注意:延伸文件名 (extension file name) 不一定是 .h 或 .cpp, 也可能是 .hpp 或其他或甚至无延伸名。

头文件防卫式声明的基本的格式:

#ifndef __COMPLEX__ 
#define __COMPLEX__   //guard防卫式声明
...
#endif

头文件的布局如下:

#ifndef __COMPLEX__ 
#define __COMPLEX__   //guard防卫式声明class ostream;        //前置声明
class complex;
complex&
__doapl (complex* ths, const complex& r);
​
class complex           //类 - 声明
{
...
};
​
complex::function ...    //类 - 定义#endif

为什么要进行防卫式声明?我们可以知道防卫式声明的作用是:防止由于同一个头文件被包含多次,而导致了重复定义。

首先看一个头文件Car.h(没有防卫式声明)

// Car.h
class Car
{
// ...
};

第二个头文件Person.h,包含了Car.h

// Person.h
#include "Car.h"
class Person
{
public:
    Car car;
};

在main.cpp里,我们同时包含两个头文件

// main.cpp
#include "Car.h"
#include "Person.h"
int main(int argc, const char * argv[]) <br>{
    Person p;
}

此时,我们会发现编译出错:Redefinition of 'Car'.

可是为什么会出现这样的情况呢?

我们需要知道,在预编译阶段,编译器会把.h文件展开,即main.cpp中的代码可以看做是:

class Car
{
    // ...
};
​
class Car
{
    // ...
};
​
class Person
{
public:
    Car car;
};
​
int main(int argc, const char * argv[]) {
    Person p;
}

在这两种声明方式中:

#ifndef 依赖于宏定义名,当宏已经定义时,#endif之前的代码就会被忽略,这里需要注意宏命名重名的问题。

#pragma once 只能保证同一个文件不会被编译多次,但是当两个不同的文件内容相同时,仍然会出错。而且这是微软提供的编译器命令,当代码需要跨平台时,需要使用宏定义方式。

3.内联函数

函数若是在类体里面定义的,便会自动成为inline的候选人。

C++中支持内联函数,其目的是为了提高函数的执行效率,用关键字 inline 放在函数定义(注意是定义而非声明)的前面即可将函数建议为内联函数,内联函数通常就是将它在程序中的每个调用点上“内联地”展开,假设我们将 max 定义为内联函数:

inline int max(int a, int b)
{
 return a > b ? a : b;
}

你用关键字inline去修饰函数只是建议编译器把此函数当作内联函数,其实有些函数也并不能成为内联函数,是否成为内联函数的看编译器。

4. access level访问级别(public、protected、private)

一般的函数声明用public修饰,数据的访问级别都用private修饰。

一些内部处理数据的函数访问级别可以设置为private或者protected。

注意:定义的对象是不可以访问直接私有的数据成员的,我们一般通过函数进行访问

class complex {
public:
    complex (double r = 0, double i = 0) : re (r), im (i)  { } 
    complex& operator += (const complex&);
    double real () const { return re; } 
    double imag () const { return im; } 
private:
    double re, im; 
    friend complex& __doapl (complex*, const complex&); 
}
{
    complex c1(2,1);//错误的访问方式
    cout << c1.re;
    cout << c1.im
}
{
    complex c1(2,1);//正确的访问方式
    cout << c1.real();
    cout << c1.imag();
}

5. 构造函数用初始化列表进行初始化

我们知道构造函数的目的是为了初始化。而初始化类的成员有两种方式,一是在构造函数体内进行赋值操作,二就是使用初始化列表,格式:初始化列表是以冒号开头,后跟一系列以逗号分隔的初始化,对于内置类型,如int,double等,使用哪一种方法好像都没有太大的差别,但是对于类类型(该类没有默认构造函数)、引用成员变量、const成员变量这三种来说,必须使用初始化列表。

class B { 
public: 
    B(int x, A a)   :_x(x)  ,_a(a) {}
private:
    int _x; 
    A _a;    //如果A类没有默认的构造函数的话,进行赋值操作编译器会报错:没有合适的默认构造函数可用
};
​
int main()
{ 
    B b(1, 10);
}

引用成员变量:

class Test
{
public:
    Test(int y)
      , _ny(y)
    {}
private:
    int& _ny;   //我们知道引用在声明的时候必须进行初始话所以这里必须要用初始化列表进行初始化
};

const成员变量的初始化也是同样的道理:

class Test
{
public:
    Test(int y)
      , _ny(y)
    {}
private:
    const int _ny; 
};

const成员变量的初始化也是同样的道理:

class Test
{
public:
    Test(int y)
      , _ny(y)
    {}
private:
    const int _ny; 
};

注意:

1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

2.尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使 用初始化列表初始化。

3.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

构造函数可以有很多个,构造函数可以进行重载(overloading ),重载后编译器会进行处理,用以区分同名重载的函数,实际的名称可能不再是我们看到的名称。

complex (double r = 0, double i = 0) : re (r), im (i)  { } 
complex () : re(0), im(0) { }
//注意:不允许这两个同时存在 假如我声明一个对象
complex a; //错误,编译器不知道调用哪个函数,存在二义性

6. 常成员函数的使用

使用const关键字修饰函数,对于函数体里面不对数据进行修改的函数我们用const进行修饰。

double real () const { return re; }//函数体里不对数据进行修改
double imag () const { return im; }

注意:声明一个const 的类的对象 调用对象 如果没有写const 可能会出现矛盾 ,编译器会报错

//下面例子假如不对函数进行const修饰
{
    complex c1(2,1); 
    cout << c1.real(); 
    cout << c1.imag();
}
{
    const complex c1(2,1); 
    cout << c1.real(); //这里会报错 const对象是不能修改的 但是这个函数确是可以修改
    cout << c1.imag();
}

7. 参数的传递(pass by value vs. pass by reference (to const))

值传递、指针传递和引用传递不同的地方,首先它们都是可以把值传递给函数的只不过是传递的方式不同,有一点是可以很明显的,指针传递和引用传递都会改变b的值,值传递不会,这就是值传递和另外的区别,而指针传递和引用传递的不同的地方则是指针传递的是b的地址,而引用传递则等于给b起了一个别名,然后通过别名来操作b的值,和它所在的内存地址。

我们的参数传递很大部分情况下传引用,像int,double基本的数据类型传不传引用影响不是很大,但是像string类作为参数的话可能就影响传递的速度了,值传递传递的是实际参数的一个副本,所以尽可能使用引用传递。

注意:对于参数不会在函数体中进行改变的参数我们要用const关键字进行操作,防止我们在函数体中进行了误操作。下面举例重写<<符号 对于参数1我们进行了修改,对于参数二我们在函数体中没有对其进行修改操作用const进行修饰。

ostream& operator << (ostream& os, const complex& x) 
{ 
    return os << '(' << real (x) << ','  << imag (x) << ')'; 
}

8. 返回值的传递引用(pass by value vs. pass by reference (to const))

传递者无需知道接受者是以reference 形式接收

inline complex& __doapl(complex* ths, const complex& r)
{ 
    ...
    return *ths;
} 
​
inline complex& complex::operator += (const complex& r)
{ 
    return __doapl(this,r);
}
​
{
    complex c1(2,1);
    complex c2(5);
    c2 += c1;
    c3 += c2 += c1; //因为返回值是引用可以进行连加操作
}

注意:对于一些函数返回的是临时对象不能以返回引用,它们返回必定是个local Object。

临时对象 typename()临时对象,到下一行就结束了如:complex(4,5); 没有名字到下一行就结束了

9.其他

1> 任何一个函数可以写成全局函数 和 成员函数

2> c++操作符要作用与左边的值

3> 特殊的操作符号<<只能是全局的函数

注意时常思考:什么情况下可以传引用,参数是否要加const修饰,函数是否要加const修饰,什么情况下返回引用,什么情况下不能返回引用,有无必要返回引用,用void 是否支持特定的操作(针对操作符重载这一块)

4>相同 class 的各個 objects 互為 friends (友元) 举例如下:

class complex {
public: 
    complex (double r = 0, double i = 0) : re (r), im (i)  { } 
    int func(const complex& param) //友元函数  可以直接的访问私有的数据成员
    { 
         return param.re + param.im; 
    }
private: 
    double re, im;
};
​
{
    complex c1(2,1);
    complex c2;
    c2.func(c1);
}