c++笔记03---构造函数,初始化表,文件分类,钟表练习,析构函数,this 指针

时间:2022-08-23 19:43:29
1.    构造函数
    class 类名 {类名(行参表){. . .}}
    成员函数名和类名一样;
    当一个对象被创建是,构造函数会自动调用被执行;
    参数来自于构造实参;
    构造函数也可以通过参数实现重载;
    构造函数不能指定返回值,连 void 都没有;

    class Student{
    private:
        string m_name;
        int m_age;                                            // 这里是声明部分,不能初始化,例如:int m_age = 20; 错!
    public:
        Student(const string &name, int age)                // 构造函数给成员变量初始化
        { m_name = name;    m_age = age; }
        Student(void)                                            // 构造函数重载,无参构造函数
        { m_name = name;    m_age = age; }

        void eat(const string &food)
        { cout << food << endl; }
    };
    int main()
    {
        Student s1 = {"zhangfei", 23};                        // ok
        Student s2 ("zhangfei", 23);                        // ok
        Student s3 = Student("zhangfei", 23);                // ok
        Student s4 ();                                        // error,编译报错,编译器把这里的s4当作函数声明
        Student s5;                                            // ok,匹配无参构造函数,建立对象s5

        Student *s6 = new Student ("zhangfei", 23);        // 堆里面创建对象,并把地址赋值给s6
        Student -> eat("kaoya");
        delete s6;
        Student &s7 = *new Student ();                        // s7是引用,需要值,所以new前面加 *
        s7.eat();
        delete &s7;

        Student ss1[3];                                        // 这个数组没参数,所以调用无参构造函数
        Student ss1[0].eat("miantiao");
        Student ss2[3] = {s1, s2};                            // 最后一个调用无参构造
        Student *ss3 = new Student[3];                        // 调用无参
        delete[] ss3;
        Student *ss4 = new Student[3] = {s1, s2, s3};        // new同时初始化,2011编译器才支持
        ss4.eat("kfc");
        delete[] ss4;
        return 0;
    }
    成员变量独占内存,成员函数可以被多个成员变量共享;
    上面这个程序有 s1 s2 s3 等多个成员变量,但在内存中,只有一个 eat() 函数,共享;

    缺省构造函数:
    如果一个类没有定义任何构造函数,那么系统就会缺省地为其提供一个无参构造函数;
    该构造函数对于基本类型的成员变量不做初始化;
    对于类类型的成员变量,调用其相应类型的无参构造函数初始化;
    如果已经有构造函数,则系统不再提供默认无参构造函数;
    class Student {
        public:
            string m_name;
            int m_age;
    }
    int main() {
        Student s;                                    // ok
        return 0;
    }
    另外:
    class Student {
        public:
            string m_name;
            int m_age;
            Student(const string &name, int age)    // 系统不再提供无参构造
    }
    int main() {
        Student s;                                    // error,编译器报错,无法调用无参构造
        return 0;
    }
    
    构造函数不能声明为 const,因为 const 构造函数是不必要的;
    构造函数的工作是初始化对象,不管对象是否为 const,都要初始化;

2.    对象构造过程:
    分配内存 -> 构造成员变量 -> 调用构造函数
    构造被调用后,对象就构造完成了;
    调用构造函数是构造一个对象最后一步;

    只要对象被构造,就会调用构造函数;
    那么,构造函数主要目的是为了初始化对象,也就是初始化对象的成员变量;
    语法上,构造函数中可以做任何事,一般情况下是作初始化工作;
    
3.    初始化表
    class 类名 {类名(行参表):初始化表{. . .}}
    class Student{
    public:
        string m_name;
        int m_age;
        student(const string &name) : m_name(name), m_age(age)        // 单参构造函数
        { }                // 如果在函数体里面又赋值,那么对应的初始化值就无效;一般初始化表和赋值二选一;
    };
    int main()
    {
        Student student ("zhangfei");
        return 0;
    }
    这里初始化表如果写为:
    student(string m_name = "zhangfei", int m_age = 30) : m_name(m_name), m_age(m_age) {}
    首先,这里函数体可以什么都不写;
    其次,行参表里声明并初始化局部变量 m_name 和 m_age;
    局部变量名和成员变量名一样,初始化表会自动区分;
    
    如果在行参表里面给参数初始化,那么就不需要单独些空参构造函数;
    如果在这种情况下还写空参构造函数,那么会编译错误,产生歧义;

4.    下面这种情况下只能用初始化表,而不能用构造函数体赋值:
    如果用函数体赋值的办法,那么编译报错,因为先有构造初始化,然后才能赋值;
    class A {
    public:
        A (int a) {};        // A有构造了,所以系统不会分配无参构造
    }
    class Student {
    public:
        string m_name;
        int m_age;
        A m_a;                // 如果无下面的初始化表,那么编译器报错,因为这里m_a会调用A的无参构造,但是A没有无参构造
        student(const string &name) : m_a(100)                // m_a 用有参A构造
        { }                    // 如果仅仅是在函数体里面赋值,则会报错;
    };

    如果类中含有常量 const 或者引用型的成员变量,必须通过初始化表对其初始化;
    class A{
    public:
        A():m_a(100) {}    // 这里如果在函数体里写成:m_a = 100; 则报错;分析如下:
    private:
        const m_a;        // 有 const,则必须用初始化表初始化;
    }
    分析:首先,m_a 是常量,m_a = 100; 这种赋值方法是错误的;
    然而,成员变量是比需要初始化的,后期赋值又没法达到初始化效果;
    那么,只能用初始化表,初始化表是在分配内存空间的时候,就进行初始化;
    
    至于引用必须用初始化表,原因是:
    引用在创建的时候,必须初始化,也就是有引用对象,所以只能用初始化表;
    在分配内存空间的时候,就进行初始化;

5.    成员变量的初始化顺序仅与其被声明的顺序有关,与初始化表的顺序无关;
    class A{
    public:
        A(char *psz):m_str(psz), m_len(m_str.length()){}        // error
    private:
        size_t m_len;
        string m_str;
    }
    // 当 m_len 调用 m_str.length() 的时候,m_str 还不存在,所以这样写是错误
    有两种办法解决上面的问题:
        第一是把 private 里面的两个声明变量交换顺序;
        第二是降低其耦合度,也就是把 m_str.length() 写成 m_str.length(psz);

6.    一般在正式项目开发中,类的声明和定义都是分开的,在不同的文件里面;
    类的声明一般在 .h 头文件里面,需注意:
        函数的默认值写在声明处;
        函数初始化列表不能写在声明处;
    类的实现/定义一般写在 .cpp 文件里,同样需注意:
        include 头文件 .h
        默认值不可以在这里出现,否则编译错误;
        初始化列别写在这里;
        所有函数的函数名前都要加上如下格式:
            类名 ::函数名(){}

    student.h 内容如下:
    #ifndef STUDENT_H
    #define STUDENT_H
    class Student{
    public:
        Student(const string &name = "", m_age = 10);            // 构造函数声明
        void print();
    private:
        string m_name;
        int m_age;
    };
    #endif
    可以用下面代码测试运行:
        g++ -c student.h
    生成:student.h.gch
    --------------------------------
    student.cpp 内容如下:
    #include <iostream>
    #include "student.h"
    Student::Student(const string &name, m_age) {...}
    void Student::print() {...}
    可以用下面代码测试运行:
        g++ -c student.cpp
    生成:student.o
    --------------------------------
    text.cpp 内容如下:
    #include "student.h"
    int main () {
        Student s;
        s.print();
        return 0;
    }
    编译时输入:
        g++ text.cpp student.cpp
    生成:text.o
    或者输入:
        g++ text.o student.o
        g++ *.o
        
    小小提示 1:我们在编写完每个文件的时候,记得在末尾空一行;
    这是由编译器决定的,如果没留,有些编译器会警告,文件末尾没留行;
    
    小小提示 2:如果我们的某一个函数特别小,可以直接在声明里面写出其实现,没必要一定分两个文件;

7.    钟表练习,可以实现计时和显示当前时间功能:
    分析:
    Clock    属性:时、分、秒;
            功能:显示show、滴答走tick;
    代码如下:

    #include <iostream>
    #include <iomanip>                                // setfill('0') 和 setw(2) 调用
    using namespace std;
    class Clock{
        public:
            Clock(bool timer = ture) : m_hour(0), m_min(0), m_sec(0) {        // 用于判断用计时器还是显示当前时间,缺省为计时器
                if (!timer) {
                    time_t t = time(NULL);            // 总秒数
                    tm *local = localtime(&t);        // 把秒数转换为结构体 tm,里面含有时分秒;
                    m_hout = local -> tm_hour;
                    m_min = local -> tm_min;
                    m_sec = local -> tm_sec;
                }
            }
            void run(void) {
                while(1) {                            // 相当于 for(;;)
                    show();                            // show无须客户看见,所以放到private里面
                    tick();                            // 同上
                }
            }
        private:                                        // 自恰性
            int m_hout;
            int m_min;
            int m_sec;
            void show() {
                cout << '\r' << setfill('0')        // 填充 0
                    << setw(2) << m_hour << ':'        // 设置为两位
                    << setw(2) << m_min << ':'
                    << setw(2) << m_sec << flush;    // flush刷新缓冲区,强制输出
                    
                // printf("\r%02d:%02d:%02d", m_hour, m_min, m_sec);
            }
            void tick() {
                sleep(1);                                // 不需要任何新的头文件
                if(++m_sec == 60) {
                    m_sec = 0;
                    if(++m_min == 60) {
                        m_min = 0;
                        if(++m_hour == 24) {
                            m_hour = 0;
                        }
                    }
                }
                /*也可以写为下面代码:
                m_sec++;                                        // 不要忘记这行代码
                time_t cur = time (0);                        // 获取当前时间
                while(time(0) == cur);                        // 休息一秒
                if(m_sec == 60) { m_sec = 0; m_min++; }
                if(m_min == 60) { m_min = 0; m_hour++; }
                if(m_hour == 24) { m_hour = 0; }
                如果这样写,记得在主函数或初始化表里给三个参数初始化为 0;
                */
            }
    };
    int main() {
        Clock clock;                // 无参调用计时器
        clock.run();
        //Clock clock(false);    // 调用时钟
        //clock.run();
        return 0;
    }

8.    this 指针
    class A{
        public:
            A(void){ cout << this << endl; };
            void foo(){ cout << this << endl; };
    };
    int main() {
        A a;
        cout << &a << endl;
        a.foo();
        A *pa = new A;
        cout << pa << endl;
        pa -> foo();
    }
    对所有成员变量的访问,通过 this 指针完成;
    一般而言,在类的成员函数或构造函数中,this关键字标志一个指针;
    对于构造函数而言,this 指向正在被构造的对象;
    对于成员函数而言,this 指向调用该函数的对象;
    
    以下初始化方式:
    int i = 45;
    void *p = &i;            // ok, void 可指代任何类型
    long *lp = &i;        // error,指针类型和对象类型需一致

9.    this 指针用途:
    1)在类的内部,区分成员变量;当全局,局部等变量名称一样的时候,this 可以区分;
        class A{
            A(int data){ this -> data = data; }        // 等号右边的是行参data
            int data; }                                // 这个data是成员变量
        如果是初始化表,则不需要且不能用this:
            A(int data):data(data){ }                // 编译器自动区分初始化表前面data为成员变量,括号里面的是参数;

    2)在成员函数中返回调用对象自身;(retthis.cpp)
        A& a() { ... return *this; }
    3)在成员函数内部,通过参数向外界传递调用对象自身,以实现对象间的交互;(atgethis.cpp)

    C++ 里不允许直接用对象作交叉类;
        class A { B m_b; };
        class B { A m_a; };
    A里有B的成员变量,B里有A的成员变量;
    但是引用或指针就可以:
        class A { B &m_b; };
        class B { A &m_a; };

10.    常函数
    如果在一个类的成员函数的参数表后面加上 const 关键字,这个成员函数就被称为常函数;
    常函数可以保护成员变量不被修改,增强安全性;
    常函数的 this 指针是一个常指针;
    在常函数内部无法修改成员变量,除非该变量具有 mutable 属性;
    mutable 具有去常功能;
    当常函数有多个变量,可以用 mutable 对其中某几个作修改;必须放在成员声明之前;
    而且,在常函数内部也无法调用非常函数;(constfuc.cpp)
    常函数和非常函数构成重载关系;
        class A {
        public:
            void foo() const
            { m_i = 100; }        // error,这里相当于:this -> m_i = 100; 而this是常指针,不允许被赋值
            int m_i;

            // 如果加上mutable,相当于去常,就可以了,代码如下:
            void foo() const
            { m_i = 100; }        // ok,这里相当于:const_cast<A*>(this)->m_i = 100;去常;
            mutable int m_i;
        };
        int main()
        {
            A a;
            a.foo();
            cout << a.m_i;
        }

11.    常对象
    拥有 const 属性的对象,对象引用或指针;
        常对象只能调用常函数;非常对象调用非常函数;
        如果没有非常函数,那么非常对象也可以调用常函数;
    同型的常函数和非常函数可以构成重载关系;
        class A{
        public:
            void foo(){}            // 和下面常函数构成重载,编译完后,参数表变为 (A *this)
            void foo()const        // 编译后参数表变为(const A *this),两者参数并不相同,所以构成重载;
            { m_i = 100; }        // error,这里相当于:this->m_i = 100;而this是常指针,不允许被赋值
            int m_i;
        };
        int main()
        {
            A a;
            a.foo();                // 调用普通 foo(),如果没有普通 foo(),那么可以调用 const foo();
            const A &r = a;
            r.foo();                // 调用常 foo()
            cout << r.m_i;
        }

12    const 函数名 (const 参数表) const {...}
        第一个 const 定义返回值;
        第二个 const 定义参数;
        第三个 const 定义 this 指针;
    全局函数没有 const 型 this 指针;

13.    析构函数
    class 类名 {~类名(void){析构函数体}}
    不返回任何值,也没有任何参数;
    当一个对象被销毁的时候,自动执行析构函数;
        局部对象离开作用域时,被销毁;
        堆对象被 delete 时,被销毁;    
    class Double {
    public:
        Double(double data) : m_data(new Double(data)) {}
        void print() const { cout << *m_data; };
        ~Double() { delete m_data; }            // 析构
    private:
        double *m_data;                            // 这个m_data属于基本类型变量,所以要专门写析构函数
    };
    int main() {
        Double d(3.14);
        d.print();                                // 这条语句执行完了,就执行析构
        Double *d1 = new Double(3.14);
        delete d1;                                // 堆对象见到delete就执行析构;
        return 0;
    }
    
14.    缺省析构:
    如果一个类没定义任何析构函数,那么系统会提供一个缺省析构函数;
        这个缺省的析构函数对基本类型的成员变量什么也不做;
        对类类型的成员变量,会调用相应类型的析构函数;

15.    一般情况下,在析构函数中,释放各种动态分配的资源;

    并不是每个类都需要析构函数;
    一般的,如果一个类的成员是按值存储,则不需要析构函数;

    一个类可以定义多个构造函数,但是析构函数只有一个,它将被应用在类的所有对象上;
    一个对象的析构函数只调用一次,但在语法上可以多次调用;

    析构函数也不局限在释放资源上;
    可以执行编写人员希望在类最后执行的程序;

    构造(加载顺序):基类--成员--子类;
    析构(释放顺序):子类--成员--基类;
    
16.    自学补充笔记:

    1)尽可能使用 const
        const* --- 被指物是常量;
        *const --- 指针自身是常量;
        *const* --- 被指物和指针都是常量;
        
    2)const 可以施加于任何作用域内的任何对象、参数、函数等,可以帮助编译器侦测出错误用法;
    3)当 const 和 non-const 成员函数有着实质等价时,令 non-const 调用 const 版本可以避免代码重复;