Coding之路——重新学习C++(4):定义一个正确的类

  我们都能定义一个类,可是如何定义一个正确的类,这是一个需要我们深入理解的问题。C++之父曾经说过定义新类型的基本思想就是将实现一个类的时候并非必要的细节(存储该类型的对象采用的布局细节)和对于这个类的正确使用至关重要的性质(访问数据的成员函数)分开设计。这种区分的最好实现方式是提供一个特定的表层接口,所有对于类内部数据结构和内部维护的调用都通过这个表层接口。

1.类该怎么定义

  (1)首先我们要明白,建立一个对象,构造函数把成员变量都放在了堆之中(除了static变量之外,static变量放在全局变量区),而所有成员函数的参数则放在了栈中。特别说明一下static静态成员变量,它是类的一部分,而不是每个类的对象的一部分,所以每个类之中仅仅只有一个,在static静态成员变量定义时,不要在头文件中定义,这样其他文件多次包含头文件的时候,静态成员变量的赋值被执行了多次,这是不允许的。另外,在类中声明static静态成员变量时需要加“static”关键字,而定义时不要加“static”关键字。

  (2)当类中有一个成员函数在逻辑上,他应该是const的,但是函数中仍然需要修改一些成员变量,我们知道这两者是矛盾的,但是有没有解决办法呢?有,那就是“mutable”,它的意思是这个成员能用能更新的方式存储,即使是const成员变量。

class Date{
    mutable bool cache_valid;
    mutable string cache;
    void compute_cache_value() const;    //填充缓存
public:
    string string_rep() const;    //字符串表示
};

string Date::string_rep() const{
    if(!cache_valid){
        compute_cache_value();
        cache_valid = true;
    }
    return cache;
}

当我们需要一个对象在逻辑上保持const但是实际上需要修改的时候,更好的选择是将需要修改的数据放到另一个独立的对象之中。

struct cache{
    bool valid;
    string rep;
};

class Date{
    cache *c;
    void compute_cache_value() const;
public:
    string string_rep() const;
}

string Date::string_rep() const{
    if(!c -> valid){
        compute_cache_value();
        c -> valid = true;
    }
    return c -> rep;
}

  (3)在C++中,在类内部定义函数都是inline函数,但是为了代码清晰,建议还是在类的定义之后再紧跟定义inline函数。

class Date{
public:
    int day() const;
    //...
private:
    int d, m, y;
    //...
};

inline int Date::day() const{
    return d;
}

  (4)下面是一个高效的用户定义Date类型,值得我们参考:

class Date{
public:            //公共界面
    enum Month {jan = 1, feb, mar, apr, may, jun, jul, aug, sep,
                           oct,nov,dec};
    class Bad_date{}    //异常类,用于异常报告错误
    Date(int dd = 0, Month mon = Month(0), int yy = 0); //0是默认值,构造函数描述如何初始化。
//查看Date的函数:
    int day() const;
    Month month() const;
    int year() const;
    string string_rep() const;
    void char_rep(char s[]) const;

//修改Date的函数
    Date& add_day(int n);
    Date& add_month(int n);
    Date& add_year(int n);
private:
    int d,m,y;
    static Date default_date;
}

  (5)除了类的成员函数,还有一些普通函数与类有关联,我们选择把它们放入一个命名空间中,使类的使用更清晰。

namespace Chrono{
    class Date{
        /*...*/
    };

    int diff(Date a, Date b);
    bool leapyear(int y);
    Date next_weekday(Date d);
}    

2.类中到底有什么

  (1)默认构造函数。默认构造函数是不用参数的构造函数,编译器生成的默认构造函数将隐式调用类成员和基类的默认构造函数。因为const和引用必须进行初始化,所以包含const和引用的类不能进行默认构造,除非显式定义了默认构造函数。

  (2)对象的复制。t1 = t2的默认含义是把t2的成员逐个复制到t1。如果类中存在指针,那么指针指向的数据只有一份,但是将会在删除t1,t2的时候被删除两次,这是相当危险的,所以复制构造函数是完成对未初始化的存储区的初始化,而复制赋值运算符必须正确处理一个结构良好的对象。

  (3)类对象作为成员。成员的构造函数在类本身的构造函数执行之前执行,这些构造函数按照成员在类中的声明顺序执行。当对象被销毁的时候,它自己的析构函数将首先被执行,而后将按照成员声明的逆顺序执行各个成员的析构函数。构造函数自下而上的构造对象,析构函数自上而下的拆除对象。另外,对于静态常量整型成员,可以在成员声明中加一个常量表达式作为初始化。

//Curious.h
class Curious{
public:
    static const int c1 = 7;        //ok,但是记得定义
    static int c2 = 11;               //错误,不是const
    const int c3 = 13;               //错误,不是static
    static const int c4 = f(17);  //错误,不是常量表达式
    static const double c5 = 7.0; //错误,不是整型
};

//Curious.cpp

const int Curious::c1;

//...

  (4)成员数组。如果在构造类的对象时有默认构造函数,不用显式提供初始式,那么就可以定义这个类的数组。

  (5)非局部对象存储。有时,我们定义一个静态的类成员,只使用一次:

class Zlib_init{
    Zlib_init();    //使Zlib能够使用
    ~Zlib_init(); //使Zlib做最后的清理
};

class Zlib{
    static Zlib_init x;
    //...
};

在一个由若干个编译单元组成的程序里,无法保证“x”这种静态成员对象一定能在它第一次使用之前初始化,在其最后使用之后销毁。我们可以采用第一次开关技术解决顺序依赖性问题,但是没有类似的最后时间开关结构:

class Zlib{
    static bool initialized;
    static void initialize() {/*初始化*/ initialized = true;}
public:
    //无构造函数
    void f(){
        if(initialized == false)
             initialize();
    }
}

  (6)临时对象。一个临时对象会在建立它的表达式结束后销毁,但是临时对象成为const引用或者用于命名对象的初始式后,还会继续存在。

  (7)对于联合,最好仅仅将它运用在底层代码,或者类实现的一部分,由类去维护联合之中存储什么信息。

  

时间: 2024-12-21 03:25:28

Coding之路——重新学习C++(4):定义一个正确的类的相关文章

Coding之路——重新学习C++(6):一个String类

这个String类是对运算符重载和以前知识的一个综合应用,提供了值语义.字符串读写.检查和不检查的访问.流I/O和字符串拼接等功能. 1.String类的定义 class String{ //类型的定义 struct Srep; //表示能被几个同样值的String共享 Srep *rep; public: class Cref; //实现下标运算,区别对待读操作和写操作 class Range(); //范围错误时抛出的异常 //构造.赋值和析构函数 String(); //x = "&quo

C++习题 对象转换(定义一个Teacher(教师)类(教师号,姓名,性别,薪金)和一个Student(学生)类(学号,姓名,性别,成绩)编写程序,将一个Student对象(学生)转换为Teacher(教师)类。

Description 定义一个Teacher(教师)类(教师号,姓名,性别,薪金)和一个Student(学生)类(学号,姓名,性别,成绩),二者有一部分数据成员是相同的,num(号码),name(姓名),sex(性别).编写程序,将一个Student对象(学生)转换为Teacher(教师)类,只将以上3个相同的数据成员移植过去.可以设想为: 一位学生大学毕业了,留校担任教师,他原有的部分数据对现在的教师身份来说仍然是有用的,应当保留并成为其教师数据的一部分. Input 一个教师的信息和一个学

Coding之路——重新学习C++(2):static的详细理解

一.C中的static关键字 1. static 局部变量 静态局部变量属于静态存储方式,它具有以下特点: (1)静态局部变量 在函数内定义它的生存期为 整个程序生命周期,但是其 作用域仍与 自动变量相同 ,只能在定义该变量的函数内使用该变量.退出该函数后,尽管该变量还继续存在,但不能使用它.(2)对基本类型的静态局部变量若在声明时未赋以初值,则系统自动赋予0值 .而对自动变量不赋初值,则其值是不定的. 根据静态局部变量的特点,可以看出它是一种生存期为整个程序生命周期.虽然离开定义它的函数后不能

Coding之路——重新学习C++(8):神奇的模板

1.解析一个正确的模板类 (1)首先,我们想创造一个模板,可以先针对一个特定的类型参数设计它的行为方式,然后在对抽象的一般类型进行推广.例如我们可以先设计String<char>类的具体实现,然后再推广到String<C>类模板. (2)类模板的名字是不能重载的.所以,如果在某个作用域内声明了一个类模板,就不能有其他同样名字的实体了. template<class T> class String{/*...*/}; class String {/*...*/}; //错

Coding之路——重新学习C++(10):类的层次结构

1.多重继承 (1)多重继承一直是C++中让许多人诟病的机制,不过它大大增加了类的层次结构的灵活性,先看一个简单的例子: class Task{ public: virtual void pending() = 0; //... }; class Displayed{ public: //... virtual void draw() = 0; }; class Satelite:public Task, public Displayed{ public: //... void pending(

Coding之路——重新学习C++(7):用继承写出一个好类

1.继承类时需要注意的地方 (1)当一个类作为基类的时候,这个类就必须有定义. (2)在派生类中,基类的默认构造函数可以被隐式调用,但是如果基类的构造函数都有参数,派生类需要直接调用一个.派生类的构造函数只能描述派生类自己的成员变量和自己的基类的直接初始式,它不能直接初始化基类的成员. Manager::Manager(const string &n, int d, int lvl) :family_name(n), //错误:在Manager里没有family_name声明 departmen

学习笔记之05-第一个OC的类

一.语法简介 1.类 在Java中,我们用1个.java文件就可以描述清楚一个类:在OC中,一般用2个文件来描述一个类: 1> .h:类的声明文件,用于声明成员变量.方法.类的声明使用关键字@interface和@end. 注意:.h中的方法只是做一个声明,并不对方法进行实现.也就是说,只是说明一下方法名.方法的返回值类型.方法接收的参数类型而已,并不会编写方法内部的代码. 2> .m:类的实现文件,用于实现.h中声明的方法.类的实现使用关键字@implementation和@end. 2.方

阶段1 语言基础+高级_1-2 -面向对象和封装_18定义一个标准的类

做一个综合的练习 如何定义一个标准的学生类 向下选择getter和setter 选中这两个 这样就自动出现了get和set的方法 生成构造方法 无参的 运行测试 全参的方式 set修改年龄 原文地址:https://www.cnblogs.com/wangjunwei/p/11198197.html

Coding之路——重新学习C++(3):对于编译和链接的重新认识

1.C++的源代码是怎么变成程序的. (1)我们在编写完源代码后,首先需要把源代码交给编译器,编译器首先进行预处理,也就是处理宏,把#include指令引进的头文件全部引进,产生编译单元.编译单元是编译器的真正工作对象,是真正意义上的C++对象. (2)一般的编译模式会采用分别编译,这时我们必须保证所有的声明具有一致性,连接器程序帮助我们把所有编译的部分都约束在一起,让所有的对象.函数没有二义性,这些都在程序运行前结束.当然,也有可以再程序运行后加入新代码(动态连接). 2.连接时的二三事(必须