• 相对于java,C++中的那些神奇语法


    空指针还可以调用成员函数

    #include 
    
    class Person {
    public:
        void sayHello() {
            printf("hello!\n");
        }
    };
    
    int main() {
        auto * p = new Person;
        p->sayHello();
    
        p = nullptr;
        p->sayHello();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    运行结果如下:

    hello!
    hello!
    
    进程已结束,退出代码为 0
    
    • 1
    • 2
    • 3
    • 4

    代码正常运行无异常,函数正常调用正常输出,真是神奇。

    需要注意的是,空指针只能调用那些没有使用成员变量的函数,否则就会抛出异常,如下:

    class Person {
    public:
        int age = 18;
        void sayHello() {
            printf("hello! age = %d\n", age);
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    再次运行代码,结果如下:

    hello! age = 18
    
    进程已结束,退出代码为 -1073741819 (0xC0000005)
    
    • 1
    • 2
    • 3

    可以看到,第二次调用sayHello()函数时程序就挂掉了,成员变量还包括this指针,如果是空指针,则this也会是空,所以可以在函数中通过this判断是否是空指针调用,如下:

    class Person {
    public:
        int age = 18;
        void sayHello() {
            if (this == nullptr) {
                printf("hello!\n");
            } else {
                printf("hello! age = %d\n", age);
            }
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    再次运行,结果如下:

    hello! age = 18
    hello!
    
    进程已结束,退出代码为 0
    
    • 1
    • 2
    • 3
    • 4

    经过这样的处理,即便函数中有成员变量,也可以在空指针中正常调用该函数了。

    类中的静态成员语法

    在这里插入图片描述
    如上截图,使用的IDE是CLion,在声明静态变量时不能直接赋值,提示非常量的静态数据成员必须在行外初始化,所以,意思是如果是常量类型的静态成员就可以罗,如下 :

    class Person {
    public:
        static const int age = 18;
    };
    
    • 1
    • 2
    • 3
    • 4

    如上代码,常量类型的静态成员变量可以直接赋值,但是常量以后就没办法修改了。如果要修改就不能声明为常量,不声明为常量就不能在声明时赋值,这真是奇怪的语法,恶心死人了,这种情况需要在类外面进行赋值,如下:

    如下:

    class Person {
    public:
        static int age;
    };
    
    int Person::age = 18;
    
    int main() {
        printf("age = %d\n", Person::age);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    对于从java转过来学习C的,这语法真的是恶心到我了。

    可以直接在文件中声明一个全局静态变量,如下:

    class Person {
    public:
        static int age;
    };
    
    int Person::age = 18;
    
    static int count = 5;
    
    int main() {
        printf("age = %d\n", Person::age);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    相对而言,类中的静态变量比文件级的静态变量在使用时多了一个类名限定符,且类中的静态变量可以加private等的修饰符。

    对于静态函数,函数体可以在类中定义,也可以在类外定义,如下:

    class Person {
    public:
        static int age;
    
        static void hello() {
            printf("hello\n");
        }
    
        static void go();
    };
    
    int Person::age = 18;
    
    void Person::go() {
        printf("go go go!");
    }
    
    
    int main() {
        Person::hello();
        Person::go();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    常函数与mutable成员变量

    这个不算奇怪,一种限制成员变量修改的语法,在java中没有,也记录一下:

    在函数后面加上const修饰,则在这个函数内不能修改成员变量,如果有某个变量确实需要修改,则在这个变量上加入mutable修饰符,示例如下:

    #include 
    
    class Person {
    public:
        int age = 18;
        mutable int count = 0;
    
        void fun1() {
            age = 19;
            count = 19;
        }
        void fun2() const {
            //age = 20; // 无法修改
            count = 20; // mutable修饰的变量可以修改
        }
    };
    
    int main() {
        Person p;
        p.fun1();
        printf("age = %d, count = %d\n", p.age, p.count);
    
        p.fun2();
        printf("age = %d, count = %d\n", p.age, p.count);
        return 0;
    }
    
    • 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

    运行结果如下:

    age = 19, count = 19
    age = 19, count = 20
    
    • 1
    • 2

    另外,在创建对象时,在对象类型的前面也可以加入const修饰,修饰后就不可修改成员变量了,但mutable类型的变量可以,在调用函数时,只能调用常函数,示例如下:

    class Person {
    public:
        int age = 18;
        mutable int count = 0;
    
        void fun1() {
        }
        void fun2() const {
        }
    };
    
    int main() {
        Person p;
        p.age = 45;
        p.count = 45;
        p.fun1();
        p.fun2();
    
        const Person p2; // 常对象
        //p2.age = 50; // 无法修改
        p2.count = 50; // mutable声明的变量可以赋值
        //p2.fun1();   // 无法调用
        p2.fun2();     // 常函数可以调用
        return 0;
    }
    
    • 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

    引用赋值/拷贝构造/赋值函数

    A.h文件如下:

    #pragma once
    #include 
    using namespace std;
    class A {
    public:
        int number;
        A(int n) {
            number = n;
        }
        A(const A & a) {
            number = a.number;
            cout << "A拷贝构造" << endl;
        }
        A & operator=(const A & a) {
            number = a.number;
            cout << "A赋值函数" << endl;
            return *this;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    main.cpp如下:

    #include "A.h"
    using namespace std;
    
    int main() {
        A a1(5);
        A a2 = a1;
        a2 = a1;
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    运行结果如下:

    A拷贝构造
    A赋值函数
    
    • 1
    • 2

    A a2 = a1;这行代码执行的是拷贝构造,a2 = a1;这行代码执行的是赋值函数。

    C的这个语法比较容易让人犯错,特别是从java过来的,在进行引用赋值时就容易出错,示例如下:

    A.h文件如下:

    #pragma once
    #include 
    using namespace std;
    class A {
    public:
        virtual void fun1() {
            cout << 1 << endl;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    B.h文件如下:

    #pragma once
    #include "A.h"
    
    class B : public A {
    public:
        void fun1() override {
            cout << 11 << endl;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    main.cpp如下:

    int main() {
        B b;
        A a = b;
        a.fun1();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们使用变量a保存了b对象,按照多态要执行b对象中的fun1()函数,可实际走的是A中的fun,因为A a = b;这句代码并不是引用赋值,而是走了a对象的拷贝构造,即调用A的拷贝构造函数创建了一个a对象,所以a.fun1()走的是A中的方法。

    再比如如下代码:

    int main() {
        B b;
        A a;
        a = b;
        a.fun1();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里依旧走的是A中的方法,a = b;走的是A对象的赋值函数,该函数默认是属性拷贝。

    友元

    这个也还好,但是后面会遇到问题,所以也记录一下。

    全局函数做友元

    示例代码如下:

    #include 
    using namespace std;
    
    class Home {
    public:
        string livingRoom = "客厅";
    private:
        string bedroom = "卧室";
    };
    
    void visit(const Home &home) {
        cout << "正在访问: " << home.livingRoom << endl;    // 公有成员可以访问
        //cout << "正在访问: " << home.bedroom << endl;     // 私有成员无法访问
    }
    
    int main() {
        visit(Home());
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    运行结果如下:

    正在访问: 客厅
    
    • 1

    如上代码,Home类中有一个public类型的livingRoom成员变量,还有一个private类型的bedroom成员变量,visit是一个全局函数,不是Home类的成员函数,所以在visit函数中,不能访问Home对象中的私有成员,就如同现实生活中,客人来了可以访问客厅,但是自己的卧室是比较隐私的,一般不想让客人进入,但是也会有个别的好朋友你是愿意他进入的。代码中也一样,有时候也希望让某些类外的方法也能访问私有的成员,此时可以把这个类外的函数设置为友元,这样它就能访问类中的私有成员了,如下:

    #include 
    using namespace std;
    
    class Home {
    
        // 设置友元
        friend void visit(const Home &home);
    
    public:
        string livingRoom = "客厅";
    private:
        string bedroom = "卧室";
    };
    
    void visit(const Home &home) {
        cout << "正在访问: " << home.livingRoom << endl;  // 公有成员可以访问
        cout << "正在访问: " << home.bedroom << endl;     // 友元可以访问私有成员
    }
    
    int main() {
        visit(Home());
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    运行结果如下:

    正在访问: 客厅
    正在访问: 卧室
    
    • 1
    • 2

    类做友元

    类设置为友元,则这整个类内都可以访问私有的成员。

    #include 
    using namespace std;
    
    class MyFriend;
    
    class Home {
    
        // 设置友元
        friend void visit(const Home &home);
        friend MyFriend;
    
    public:
        string livingRoom = "客厅";
    private:
        string bedroom = "卧室";
    };
    
    class MyFriend {
    public:
        void visit(const Home &home) {
            cout << "正在访问: " << home.livingRoom << endl;
            cout << "正在访问: " << home.bedroom << endl;
        }
    };
    
    void visit(const Home &home) {
        cout << "正在访问: " << home.livingRoom << endl;
        cout << "正在访问: " << home.bedroom << endl;
    }
    
    int main() {
        visit(Home());
        MyFriend myFriend;
        myFriend.visit(Home());
        return 0;
    }
    
    • 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

    成员函数做友元

    可以设置只允许类中的某个函数做友元,如下:

    #include 
    using namespace std;
    
    class Home;
    
    class MyFriend {
    public:
        void visit(const Home &home);
    };
    
    class Home {
        friend void MyFriend::visit(const Home &home);
    
    public:
        string livingRoom = "客厅";
    private:
        string bedroom = "卧室";
    };
    
    void MyFriend::visit(const Home &home) {
        cout << "正在访问: " << home.livingRoom << endl;
        cout << "正在访问: " << home.bedroom << endl;
    }
    
    int main() {
        MyFriend().visit(Home());
        return 0;
    }
    
    
    • 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

    注意如下代码:

    class Home;
    
    class MyFriend {
    public:
        void visit(const Home &home);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    因为Home定义在MyFriend的后面,而在MyFriend中又用到了Home,所以在前面使用class Home声明一下,告诉编译器有Home这个类。

    我们把HomeMyFriend的定义换一下位置:

    class MyFriend;
    
    class Home {
        friend void MyFriend::visit(const Home &home);
    
    public:
        string livingRoom = "客厅";
    private:
        string bedroom = "卧室";
    };
    
    class MyFriend {
    public:
        void visit(const Home &home);
    };
    
    void MyFriend::visit(const Home &home) {
        cout << "正在访问: " << home.livingRoom << endl;
        cout << "正在访问: " << home.bedroom << endl;
    }
    
    int main() {
        MyFriend().visit(Home());
        return 0;
    }
    
    • 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

    这个代码是有问题的,无法编译,在CLion中显示错误如下:
    在这里插入图片描述
    这是因为虽然在前面声明了class MyFriend;,但在Home中使用到了MyFriendvisit函数,这是不行的,因为声明MyFriend只能表明有这样一个类,但是无法知道这个类里面有什么的,于是想到能否把MyFriendvisit函数也声明一下呢?如下:
    在这里插入图片描述
    如上图,是不是写法有问题,再如下面:
    在这里插入图片描述
    如上图,还是有问题,那我把visit的定义写到前面呢?如下:
    在这里插入图片描述
    如上图,也是不行。所以,结论是Home必须定义在MyFriend后面,这奇怪的语法真是令人难记,头痛,怎么记得住谁要定义在谁的前面。再来看正确的代码,如下:

    #include 
    
    #include 
    using namespace std;
    
    class Home;
    
    class MyFriend {
    public:
        void visit(const Home &home);
    };
    
    class Home {
        friend void MyFriend::visit(const Home &home);
    
    public:
        string livingRoom = "客厅";
    private:
        string bedroom = "卧室";
    };
    
    void MyFriend::visit(const Home &home) {
        cout << "正在访问: " << home.livingRoom << endl;
        cout << "正在访问: " << home.bedroom << endl;
    }
    
    int main() {
        MyFriend().visit(Home());
        return 0;
    }
    
    
    • 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

    这里visit是定义在类外面的,我们把它定义到类里面,此时就又报错了,如下:

    在这里插入图片描述
    这是因为Home是定义在MyFriend后面的,所以在MyFriend中无法知道Home中的成员。

    后面经过跟公司同事交流,说真实开发中没人会把类都写一个文件,一般是每个类单独写一个头文件和cpp源文件,这样在使用include的时候就不会有这个问题,代码如下:

    Home.h如下:

    #ifndef WINDOWSAPP_HOME_H
    #define WINDOWSAPP_HOME_H
    
    #include "MyFriend.h"
    #include 
    using namespace std;
    
    class Home {
        friend void MyFriend::visit(const Home &home);
    
    public:
        string livingRoom = "客厅";
    private:
        string bedroom = "卧室";
    };
    
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    MyFriend.h如下:

    #ifndef WINDOWSAPP_MYFRIEND_H
    #define WINDOWSAPP_MYFRIEND_H
    
    class Home;
    
    class MyFriend {
    public:
        void visit(const Home & home);
    };
    
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    MyFriend.cpp如下:

    #include "MyFriend.h"
    #include "Home.h"
    
    void MyFriend::visit(const Home & home) {
        cout << "MyFriend正在访问: " << home.livingRoom << endl;
        cout << "MyFriend正在访问: " << home.bedroom << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    main.cpp如下:

    #include "Home.h"
    using namespace std;
    
    int main() {
        MyFriend myFriend;
        myFriend.visit(Home());
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    此时运行代码是正常的。

    注意,我们在MyFriend.h 中声明了Home类:class Home;,既然都单独写头文件了,为何不直接include呢,于是修改为如下:

    #ifndef WINDOWSAPP_MYFRIEND_H
    #define WINDOWSAPP_MYFRIEND_H
    
    #include "Home.h"
    
    class MyFriend {
    public:
        void visit(const Home & home);
    };
    
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    改了这个之后,MyFriend.cpp中就报错了,如下:
    在这里插入图片描述
    这是因为两个类不能互相包含,详情可看后面的知识

    两个头文件相互include

    A.h如下:

    #pragma once
    #include "B.h"
    class A
    {
    public:
    	B b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    B.h如下:

    #pragma once
    #include "A.h"
    class B
    {
    public:
    	A a;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    当我们创建一个A对象的时候,如:A a;由于A里面需要创建一个B,而创建B时又需要创建A,这会导致死循环创建,就如同Java中的如下代码:

    public class A {
        public B b = new B();
    }
    
    public class B {
        public A a = new A();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这肯定会导致内存益出,解决的办法就是不要在声明的时候就直接赋值,修改为如下:

    public class A {
        public B b;
    }
    
    public class B {
        public A a;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    后期再给A、B中的成员属性赋值就可以了,C++中也一样,可以把成员变量声明为指针,这样就不会一开始就创建对象了,如下:

    A.h如下:

    #pragma once
    #include "B.h"
    class A
    {
    public:
    	B * b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    B.h如下:

    #pragma once
    #include "A.h"
    class B
    {
    public:
    	A * a;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个代码在IDE中是没有报错的,但是运行时会报错,这是因为A和B相互include,虽然前面有#pragma once,还是不行,所以要改为类声明,如下:
    A.h如下:

    #pragma once
    class B;
    class A
    {
    public:
    	B * b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    B.h如下:

    #pragma once
    class A;
    class B
    {
    public:
    	A * a;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这样就没问题了,main.cpp如下:

    #include "A.h"
    #include "B.h"
    
    int main()
    {
        A a;
        B b;
        a.b = &b;
        b.a = &a;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    只要不是两个头文件互相包含,也是可以的,比如在A中include B,此时在B中就不能inlclude A了,只能使用class A;来声明一下。一般来说,在头文件里面,能使用类声明,就不要使用include,迫不得已的情况下才使用include,比如,A类在设置B类中的某个方法为友元时就需要使用include,如下:

    #pragma once
    #include "MyFriend.h"
    #include 
    using namespace std;
    
    class Home {
        friend void MyFriend::visit(const Home &home);
    
    public:
        string livingRoom = "客厅";
    private:
        string bedroom = "卧室";
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    如上代码,我们在Home中设置MyFriend类的visit(const Home &home)函数为友元函数,以便该函数可以访问Home中的私有变量,所以在Home中,我们需要知道MyFriend类,还需要知道它有一个函数叫visit(const Home &home),所以需要#include "MyFriend.h",如果只是使用class MyFriend;来声明一下,这就无法知道MyFriend类中有visit(const Home &home)函数了。

    有一种情况可以相互include,如下代码:

    A.h如下:

    #pragma once
    #include "B.h"
    
    class A {
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    B.h如下:

    #pragma once
    #include "A.h"
    
    class B {
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如上代码,A和B相互include,但是编译运行是没问题的,公司同事说这是因为A类中并没有使用到B类,所以编译时#include "B.h"并不会被展开,会被编译器优化掉,因为编译器知道A中并没有使用到B,所以根本不需要用到#include "B.h",所以会被优化掉。

    如上所说都是猜想,实际还需要我们进行实验,究其根本,这样才能看其本质,才能深刻理解,不然光靠死记硬背是很难记忆的。请看下面知识点“使用函数或类时,必须在前面有声明”。

    使用函数或类时,必须在前面有声明

    C里面很奇怪的就是在调用一个函数时,必须在前面声明有这个函数或类,否则编译就通不过,java就没有这种限制,在java中,两个函数声明顺序随意调整也没事,如果不是当前类中的函数,则只需要import即可。C的这种奇怪限制可是使开发的事情变复杂了,如下:

    在这里插入图片描述
    如上代码,main函数中调用了max函数,在IDE中直接就报错了,C是从上到下编译的,当编译到main函数中调用max时,发现这个函数之前没有定义过,所以编译不给通过,分步编译,如下:

    1. 预编译

      g++ -E main.cpp -o main.i
      
      • 1

      这是预处理命令,不会对语法进行检查,所以命令执行无异常,正常生成main.i文件,打开该文件,内容如下:

      # 1 "main.cpp"
      # 1 ""
      # 1 ""
      # 1 "main.cpp"
      
      int main() {
          int max = max(10, 20);
      }
      
      int max(int a, int b) {
          return a > b ? a : b;
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
    2. 把C++翻译为汇编语言

      g++ -S main.i -o main.s
      
      • 1

      这一步是会进行语法检查的,此时就报错了,如下:
      在这里插入图片描述
      如上图所示,提示我们max不能作为函数使用,换句话说就是max是不是一个函数,编译器认为它不是一个函数,因为编译器在编译前面的内容时没有发现该函数的定义或声明,编译是从上到下进行的。

    此时可以把max函数的定义写到main函数前面,又或者在main函数前面声明一下max函数,如下:
    在这里插入图片描述
    可以看到,max函数的调用处还是报错,我们再执行之前的编译步骤,如下:
    在这里插入图片描述
    可以看到,还是一样的错误,这其实是因为max就是和max函数名同名了,这对于从java转过来的,还真不知道这个奇怪的知识点,我们把max变量改为m变量,如下:
    在这里插入图片描述
    这下就没有报错了,编译也能正常通过了。

    假设我们有一个源文件中有10个函数,在另一个源文件中需要用到这10个函数,则需要在这个源文件中声明这10个函数,这样太累了,所以C语言中有include语法,可以把声明写在头文件中,以后只需要include即可,在进行编译时,它会把include 的头文件中的内容直接复制过来,示例如下:

    math.cpp文件:

    int max(int a, int b) {
    	return a > b ? a : b;
    }
    
    int min(int a, int b) {
    	return a < b ? a : b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    math.h文件:

    int max(int a, int b);
    int min(int a, int b);
    
    • 1
    • 2

    main.cpp文件:

    #include "math.h"
    
    int main() {
        int max_result = max(10, 20);
        int min_result = min(10, 20);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们执行预编译命令:g++ -E main.cpp -o main.i

    查看生成的main.i文件,如下:

    # 1 "main.cpp"
    # 1 ""
    # 1 ""
    # 1 "main.cpp"
    # 1 "math.h" 1
    int max(int a, int b);
    int min(int a, int b);
    # 2 "main.cpp" 2
    
    int main() {
        int max_result = max(10, 20);
        int min_result = min(10, 20);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    可以看到,include其实就是会把头文件中的内容复制过来。而且include还可以是间接的,示例如下:

    min.h文件:

    int min(int a, int b);
    
    • 1

    max.h文件:

    #include "min.h"
    
    int max(int a, int b);
    
    • 1
    • 2
    • 3

    main.cpp文件:

    #include "max.h"
    
    int main() {
        int max_result = max(10, 20);
        int min_result = min(10, 20);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如上代码,有两个头文件,max.hincludemin.hmain.cppincludemax.h。注意,此时我们的max.hmin.h并没有对应的.cpp文件,也就是说这里面的两个函数只有声明,没有定义。执行预编译命令:g++ -E main.cpp -o main.i,查看main.i文件,如下:

    # 1 "main.cpp"
    # 1 ""
    # 1 ""
    # 1 "main.cpp"
    # 1 "max.h" 1
    # 1 "min.h" 1
    int min(int a, int b);
    # 2 "max.h" 2
    
    int max(int a, int b);
    # 2 "main.cpp" 2
    
    int main() {
        int max_result = max(10, 20);
        int min_result = min(10, 20);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    可以看到,虽然我们在main.cpp中只是includemax.h,但是min.h中的函数声明也被复制进来了,这就是间接include,预编译时,在main.cpp文件中遇到#include "max.h",然后max.h中的第一行是#include "min.h",所以会先把min.h中的函数声明先复制到main.cpp中,然后才是复制max.h中的函数。

    接下来执行汇编命令,把C语言转换为汇编语言:g++ -S main.i -o main.s,这里没有报错,我们只是声明了maxmin函数,并没有对应的函数定义,这说明在把C语言翻译为汇编的这一步,只检查语法是否正确,但是函数没定义它是不管的,只要有声明即可,接下来再执行第三步编译,把没汇编语言翻译为机器语言g++ -c main.s -o main.o,跟上一步一样,也没报错,接下来再执行最后一步编译,链接动态库并生成可执行文件:g++ main.o -o main.exe,这里就报错了,因为要生成最终可执行文件了,然后我们的maxmin函数并没有定义,所以报错了,如下:
    在这里插入图片描述
    我们把这两个函数的定义补上,如下:

    max.cpp文件:

    int max(int a, int b) {
    	return a > b ? a : b;
    }
    
    • 1
    • 2
    • 3

    min.cpp文件:

    int min(int a, int b) {
    	return a < b ? a : b;
    }
    
    • 1
    • 2
    • 3

    再次执行4个编译步骤,我们需要分别把main.cppmax.cppmin.cppp分别编译为一对应的main.omax.omin.o文件,然后再执行最后的把3个文件整合编译为一个exe文件:g++ main.o max.o min.o -o main.exe,如下:
    在这里插入图片描述
    如上图,可以看到,分步编译是比较麻烦的,需要把每个cpp文件编译对应的.o文件,然后再把所有的.o文件一起编译为一个.exe文件。这个是我们自己手动分步编译,也可以让程序自动完成这些分步编译,看起来就是一步编译一样,如下:

    g++ main.cpp max.cpp min.cpp -o main.exe
    
    • 1

    执行效果如下:
    在这里插入图片描述
    如果有100个cpp文件,这命令写起来也是要命,所以有更简单的命令:

    g++ *.cpp -o main.exe
    
    • 1

    可以看到,一步到位,其实它底层也是分多个步骤来完成编译的,只是这些步骤由编译程序自动完成。这种编译方式不会产生中间文件,也就是我们看不到.i.s.o这些中间文件,只有.exe文件。

    C语言中的这种include机制,有时候会出问题,在前面的示例中,我们修改一下main.cpp文件,多增加一个include语句,如下:

    // main.cpp文件
    #include "max.h"
    #include "min.h"
    
    int main() {
        int max_result = max(10, 20);
        int min_result = min(10, 20);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    然后执行预编译命令:``

    查看生成的main.i文件,如下:

    # 1 "main.cpp"
    # 1 ""
    # 1 ""
    # 1 "main.cpp"
    # 1 "max.h" 1
    # 1 "min.h" 1
    int min(int a, int b);
    # 2 "max.h" 2
    
    int max(int a, int b);
    # 2 "main.cpp" 2
    # 1 "min.h" 1
    int min(int a, int b);
    # 3 "main.cpp" 2
    
    int main() {
        int max_result = max(10, 20);
        int min_result = min(10, 20);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    可以看到,min函数的声明重复了,这是因为在max.h中,我们include了一次min.h,在main.cpp中又include了一次min.h,虽然声明重复了,但是并不影响运行,也就是说一个函数被声明多次也是OK的。但是,如果两个头文件存在相互include就不一样,如下:
    max.h文件如下:

    #include "min.h"
    
    int max(int a, int b);
    
    • 1
    • 2
    • 3

    min.h文件如下:

    #include "max.h"
    
    int min(int a, int b);
    
    • 1
    • 2
    • 3

    main.cpp文件如下:

    #include "max.h"
    
    int main() {
        int max_result = max(10, 20);
        int min_result = min(10, 20);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    执行预编译命令:g++ -E main.cpp -o main.i,执行效果如下:

    在这里插入图片描述
    可以看到,编译错误,提示说include的嵌套太深了,我们在main.cpp中写了#include "max.h",于是编译器去展开max.h,但是max.h的第一行代码是:#include "min.h",于是编译器又去展开min.h,但是min.h的第一句代码是#include "max.h",于是又去展开max.h,就这样一直嵌套,无限循环嵌套,这肯定是怎么也无法最终展开的,所以提示我们include的嵌套太深了。为了预言这个重复include的问题,可以在头文件的最前面加入一句:#pragma once,如下:

    max.h文件:

    #pragma once
    #include "min.h"
    
    int max(int a, int b);
    
    • 1
    • 2
    • 3
    • 4

    min.h文件:

    #pragma once
    #include "max.h"
    
    int min(int a, int b);
    
    • 1
    • 2
    • 3
    • 4

    再次执行预编译命令:g++ -E main.cpp -o main.i,然后查看生成的main.i文件,如下:

    int min(int a, int b);
    int max(int a, int b);
    
    int main() {
        int max_result = max(10, 20);
        int min_result = min(10, 20);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里已经删掉了一些不必要带#开头的代码,可以看到,这次的函数声明就没有重复出现了,编译时,在main.cpp中遇到#include "max.h",于是去展开max.h,第一行遇到#pragma once,它的功能为如果之前没展开过这个文件,则展开,如果展开过了,则不再展开了,所以第一次会执行展开操作,好那么就会往下展开,在max.h中往下走遇到#include "min.h",于是又转而去展开min.h,这个文件的第一句代码也是#pragma once,这个文件之前没展开过,于是执行展开操作,往下走遇到min.h的第二行代码是#include "max.h",于是又去展开max.h,这个头文件的第一行代码是#pragma once,此时max.h之前已经展开过了,于是不再执行展开操作了,回到min.h中,开始执行第三行代码,第三行代码是一个函数声明:int min(int a, int b);,于是这个声明就被复制到main.cpp文件中了,这样min.h的展开操作就完全结束了,回到之前的max.h,它的第二行代码#include "min.h"执行完成,于是执行第三句:int max(int a, int b);,这也是一个函数声明,于是被复制到了main.cpp中,到这里max.h的展开操作也完全结束了。所以,只要在写头文件时,在前面都加入#pragma once,这样就可以解决重复包含的问题。

    虽然解决了相互包括的问题,但是我们尽量不要相互包括,因为相互包含还是会出问题的,示例如下:

    A.h如下:

    #pragma once
    #include "B.h"
    
    class A
    {
    public:
    	B * b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    B.h如下:

    #pragma once
    #include "A.h"
    
    class B
    {
    public:
    	A * a;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    main.cpp如下:

    #include "A.h"
    
    int main() {
        A a;
    	B b;
    	a.b = &b;
    	b.a = &a;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    因为A.hB.h是相互包含的,所以在main.cpp中,我们只需要包含其中一个头文件即可。

    • 执行预编译:g++ -E main.cpp -o main.i
    • 执行汇编编译:g++ -S main.i -o main.s

    在执行第二步编译的时候报错了,如下:
    在这里插入图片描述
    这里有两个错误提示,第一个提示说A不是一个类型,第二个说B里面没有成员变量A,为什么会这样呢?我们可以查看main.i文件,如下:

    class B
    {
    public:
     A * a;
    };
    
    class A
    {
    public:
     B * b;
    };
    
    int main() {
        A a;
        B b;
        a.b = &b;
        b.a = &a;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    可以看到,因为头文件中有#pragma once,所以即便A.hB.h互相包含,但main.cpp中并没有出现重复定义,也正因为此出了问题。在class B里面使用了A,但是编译器编译到这里的时候发现A并没有定义,所以就报错了,这也能解决为什么报错提示这个:B.h:7:2: error: 'A' does not name a type

    了解了本质问题之后,解决起来就很简单了,只要使用类声明就解决了,如下:

    A.h如下:

    #pragma once
    #include "B.h"
    
    class B;
    
    class A
    {
    public:
    	B * b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    B.h如下:

    #pragma once
    #include "A.h"
    
    class A;
    
    class B
    {
    public:
    	A * a;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    再次编译main.cpp,并查看main.i文件,如下:

    class A;
    
    class B
    {
    public:
     A * a;
    };
    
    class B;
    
    class A
    {
    public:
     B * b;
    };
    
    int main() {
        A a;
     B b;
     a.b = &b;
     b.a = &a;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    这次编译就没问题了,所以前面在解友元和两个头文件相互包含的时候,说最好两个头文件不要相互include,其实不是完全正确的,了解了其本质之后,我们知道问题并不是出在相互include,我只只需要看哪边缺少声明,就给哪边补上类声明即可,当然最好是两边都被上,因为我们无法确定最终在使用的时候哪个类的定义会被include在前面。

    这里我有一个问题,类可以添加声明,文件级别的函数也可以添加声明,那类成员函数如何添加声明?

    前面还提到过有一种情况可以相互包含没问题,如下:

    a.h如下:

    #pragma once
    #include "b.h"
    
    class A {
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    b.h如下:

    #pragma once
    #include "a.h"
    
    class B {
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    main.cpp如下:

    #include "a.h"
    
    int main() {
        A a;
    	B b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这代码编译运行是没问题的,之前问我们同事说是编译器优化了所以没问题,我们看其中一个头文件如下:

    #pragma once
    #include "b.h"
    
    class A {
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如上代码,在A类中#include "b.h",因为编译器知道A类中并没有使用到B,所以不会展开这个头文件,所以没问题,这是不对的,我们使用预编译命令:g++ -E main.cpp -o main.i,查看main.i文件,如下:

    class B {
    
    };
    
    class A {
    
    };
    
    int main() {
        A a;
        B b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    再回看main.cpp,我们只有#include "a.h",编译器在展开a.h文件的时候,发现a.h文件中又有#include "b.h",所以又会去展开b.h,b.h中又有#include "a.h",但是这个文件之前已经展开过了,所以不再展开,所以往下走就是把b.h中的class B { };定义复制到main.cpp中,b.h展开结束,然后回到a.h,此时又会把class A { };定义复制到main.cpp中。所以,总结就是,即使我们没有使用到include的头文件中声明的东西,但是只要我们调用了include,则include的头文件中的内容都会被展开。之所有这里的互相包含没问题,为什么 没问题,看预编译后的main.i文件即可知道,这是合法的,确实不会有问题。

    引用类型的函数重载

    java中,基本数据类型是没有引用类型的,而C++中是有的。且在java中,修饰符不同是当成同一个参数的,如下:

    在这里插入图片描述
    C++中也一样:
    在这里插入图片描述
    但是如果多个函数的参数是引用类型,则可以重发重载,如下:

    void fun(int & a) {
        a = 50;
        cout << "重载函数2" << endl;
    }
    
    void fun(const int & a) {
        cout << "重载函数3" << endl;
    }
    
    int main() {
        int a = 60;
        const int & b = a;
        fun(a);
        fun(b);
        cout << a << endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    如上代码,两个fun函数,int & aconst int & a被认为是两个不同的参数,可以发生函数重载。运行结果a为50,说明fun函数中的a和main函数中的a是同一个,所以可以修改,这在java是不行的,java对于基本数据类型都是值传递,没有引用传递。

    C++中,如果有原来的类型,则引用类型的函数不能发生重载,如下 :

    void fun(int a) {
        cout << "重载函数1" << endl;
    }
    
    void fun(int & a) {
        cout << "重载函数2" << endl;
    }
    
    void fun(const int & a) {
        cout << "重载函数3" << endl;
    }
    
    int main() {
        int a = 60;
        fun(a);
        cout << a << endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    CLion IDE中显示这3个fun函数定义没有报错,但是在调用fun(a);时报错了,提示说这个调用模棱两可,因为这种情况下无法理解用户想要调用的是哪个函数。即使我们使用一个引用类型也不行,如下:

    int main() {
        int a = 60;
        int & b = a;
        fun(b);
        cout << a << endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    类的成员修饰符和虐函数

    成员变量

    对于Java,成员变量没有覆盖说法,所以子类的相同名称的成员修饰符可以随意修改,而成员方法有覆盖的说法,private函数无法覆盖,protected函数在覆盖时可以改为public,而public函数在覆盖时无法改为protected,也就是说访问权限可以由小改大,但是不能由大改小,示例如下:

    public class Person {
    
        public int field1 = 1;
        protected int field2 = 2;
        private int field3 = 3;
    
        public int fun1() {
            return 1;
        }
    
        protected int fun2() {
            return 2;
        }
    
        private int fun3() {
            return 2;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    public class Student extends Person {
    
        private int field1 = 11; // 修饰符可随意修改
        private int field2 = 22; // 修饰符可随意修改
        public int field3 = 33;  // 修饰符可随意修改
    
        @Override
        public int fun1() { // 不能改为protected或private
            return 11;
        }
    
        @Override
        protected int fun2() { // 可以改为public
            return 22;
        }
    
        // 无法覆盖fun3函数
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    对于C++的控制方面,比java要多得多,在C++中使用子类的对象引用也可以访问到父类的成员,示例如下:

    #pragma once
    class Person {
    public:
        int field1 = 1;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    #pragma once
    #include "Person.h"
    class Student : public Person {
    public:
        int field1 = 11;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    #include 
    #include "Student.h"
    using namespace std;
    
    int main() {
        Student stu;
        cout << stu.field1 << endl;
        cout << stu.Person::field1 << endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在类上加修饰符,可以修改原有访问权限,但是只能大的改小,小的改不成大,比如在类上加入public,则相当于完全使用父类的修饰符,如下:

    在这里插入图片描述
    如上代码,A类中有三个不同修饰符的成员变量,B继承于A。

    可以看到protected的变量在继承它的类上可以访问,在main函数中无法访问,而java中是可以的。

    接下来,我们把B类中的public改为protected,则在main函数中field1也不能访问了,即使使用父类引用也无法访问,如下:
    在这里插入图片描述
    在子类中,也可以声明同名成员变量,此时的修饰符就可以随便写都可以,因为它没有覆盖的概念,相当于是新的成员变量,如下:

    在这里插入图片描述
    如上代码,B类中声明的3个成员变量都是public,在main函数中可以访问,但如果访问A中的public类型的field1,则不行,因为这个变量已经被B为中继承时的protected修改了,所以,类上面修饰符只是修改父类中的访问权限,验证如下:
    在这里插入图片描述
    可以看到,只有访问A中的成员受到了影响。

    成员函数

    在这里插入图片描述
    运行结果如下:

    11
    1
    
    • 1
    • 2

    如上代码,B继承于A,在main函数中创建了B对象,并使用A类型变量保存了B变量,在调用a.fun1()时,走的是A类中的fun1函数,这是因为B类并没有覆盖A类中的fun1函数,所以没有多态的功能,虽然变量a保存了b对象引用,但是a是A类型的,在这编译时就绑定了A中的函数了,所以调用的就是A中的函数,要想实现覆盖,需要在子类的函数后面加入override,且父类中被覆盖的函数要使用virtual修饰,如下:

    在这里插入图片描述
    此时调用的就是B中的函数了。且这里我们可以看到,私有函数也可以覆盖,验证如下:
    在这里插入图片描述
    输出结果为33,A中fun1()中调用的fun3()是B类中的fun3()说明这是实现了覆盖且多态调用。

    与成员属性一样,子类中的函数修饰符也是可以随意修改的,比如父类为private,子类也可改为public
    在这里插入图片描述
    在类上可以做统一修改,这是指不覆盖的情况,这种修改是改小不改大,比如在类上使用public,则修饰符完全与父类一样,如果在类上使用protected,则父类中public会被降级为protected,其它不变,如果在类上使用private,则父类中的publicprotected会被降级为private,示例如下:

    在这里插入图片描述
    在这里插入图片描述

  • 相关阅读:
    单源广度优先搜索 (leetcode经典例题 C++实现)
    全网最完整php 禁止eval函数讲解
    ionic cordova项目修改插件
    欧洲fba海运详解:欧洲fba海运怎么样?有哪些优势?
    爬虫逆向实战(30)-某查查股东关联公司(HmacSHA512)
    WPF中动画教程(DoubleAnimation的基本使用)
    python循环引用解决 cannot import partially initialize
    SpringBoot原理篇
    linux php-fpm进程 cpu占用过高 解决方法
    centos7环境下安装jdk8
  • 原文地址:https://blog.csdn.net/android_cai_niao/article/details/127516423