您当前的位置:首页 > 计算机 > 编程开发 > VC/VC++

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

时间:02-10来源:作者:点击数:

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

#include <cstdio>

class Person {
public:
    void sayHello() {
        printf("hello!\n");
    }
};

int main() {
    auto * p = new Person;
    p->sayHello();

    p = nullptr;
    p->sayHello();
    return 0;
}

运行结果如下:

hello!
hello!

进程已结束,退出代码为 0

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

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

class Person {
public:
    int age = 18;
    void sayHello() {
        printf("hello! age = %d\n", age);
    }
};

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

hello! age = 18

进程已结束,退出代码为 -1073741819 (0xC0000005)

可以看到,第二次调用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);
        }
    }
};

再次运行,结果如下:

hello! age = 18
hello!

进程已结束,退出代码为 0

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

类中的静态成员语法

在这里插入图片描述

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

class Person {
public:
    static const int age = 18;
};

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

如下:

class Person {
public:
    static int age;
};

int Person::age = 18;

int main() {
    printf("age = %d\n", Person::age);
    return 0;
}

对于从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;
}

相对而言,类中的静态变量比文件级的静态变量在使用时多了一个类名限定符,且类中的静态变量可以加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;
}

常函数与mutable成员变量

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

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

#include <cstdio>

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;
}

运行结果如下:

age = 19, count = 19
age = 19, count = 20

另外,在创建对象时,在对象类型的前面也可以加入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;
}

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

A.h文件如下:

#pragma once
#include <iostream>
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;
    }
}

main.cpp如下:

#include "A.h"
using namespace std;

int main() {
    A a1(5);
    A a2 = a1;
    a2 = a1;
    return 0;
}

运行结果如下:

A拷贝构造
A赋值函数

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

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

A.h文件如下:

#pragma once
#include <iostream>
using namespace std;
class A {
public:
    virtual void fun1() {
        cout << 1 << endl;
    }
};

B.h文件如下:

#pragma once
#include "A.h"

class B : public A {
public:
    void fun1() override {
        cout << 11 << endl;
    }
};

main.cpp如下:

int main() {
    B b;
    A a = b;
    a.fun1();
    return 0;
}

我们使用变量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;
}

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

友元

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

全局函数做友元

示例代码如下:

#include <iostream>
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;
}

运行结果如下:

正在访问: 客厅

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

#include <iostream>
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;
}

运行结果如下:

正在访问: 客厅
正在访问: 卧室

类做友元

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

#include <iostream>
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;
}

成员函数做友元

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

#include <iostream>
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;
}

注意如下代码:

class Home;

class MyFriend {
public:
    void visit(const Home &home);
};

因为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;
}

这个代码是有问题的,无法编译,在CLion中显示错误如下:

在这里插入图片描述

这是因为虽然在前面声明了class MyFriend;,但在Home中使用到了MyFriendvisit函数,这是不行的,因为声明MyFriend只能表明有这样一个类,但是无法知道这个类里面有什么的,于是想到能否把MyFriendvisit函数也声明一下呢?如下:

在这里插入图片描述

如上图,是不是写法有问题,再如下面:

在这里插入图片描述

如上图,还是有问题,那我把visit的定义写到前面呢?如下:

在这里插入图片描述

如上图,也是不行。所以,结论是Home必须定义在MyFriend后面,这奇怪的语法真是令人难记,头痛,怎么记得住谁要定义在谁的前面。再来看正确的代码,如下:

#include <iostream>

#include <iostream>
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;
}

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

在这里插入图片描述

这是因为Home是定义在MyFriend后面的,所以在MyFriend中无法知道Home中的成员。

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

Home.h如下:

#ifndef WINDOWSAPP_HOME_H
#define WINDOWSAPP_HOME_H

#include "MyFriend.h"
#include <iostream>
using namespace std;

class Home {
    friend void MyFriend::visit(const Home &home);

public:
    string livingRoom = "客厅";
private:
    string bedroom = "卧室";
};

#endif

MyFriend.h如下:

#ifndef WINDOWSAPP_MYFRIEND_H
#define WINDOWSAPP_MYFRIEND_H

class Home;

class MyFriend {
public:
    void visit(const Home & home);
};

#endif

MyFriend.cpp如下:

#include "MyFriend.h"
#include "Home.h"

void MyFriend::visit(const Home & home) {
    cout << "MyFriend正在访问: " << home.livingRoom << endl;
    cout << "MyFriend正在访问: " << home.bedroom << endl;
}

main.cpp如下:

#include "Home.h"
using namespace std;

int main() {
    MyFriend myFriend;
    myFriend.visit(Home());
    return 0;
}

此时运行代码是正常的。

注意,我们在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

改了这个之后,MyFriend.cpp中就报错了,如下:

在这里插入图片描述

这是因为两个类不能互相包含,详情可看后面的知识

两个头文件相互include

A.h如下:

#pragma once
#include "B.h"
class A
{
public:
	B b;
};

B.h如下:

#pragma once
#include "A.h"
class B
{
public:
	A a;
};

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

public class A {
    public B b = new B();
}

public class B {
    public A a = new A();
}

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

public class A {
    public B b;
}

public class B {
    public A a;
}

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

A.h如下:

#pragma once
#include "B.h"
class A
{
public:
	B * b;
};

B.h如下:

#pragma once
#include "A.h"
class B
{
public:
	A * a;
};

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

A.h如下:

#pragma once
class B;
class A
{
public:
	B * b;
};

B.h如下:

#pragma once
class A;
class B
{
public:
	A * a;
};

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

#include "A.h"
#include "B.h"

int main()
{
    A a;
    B b;
    a.b = &b;
    b.a = &a;
}

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

#pragma once
#include "MyFriend.h"
#include <iostream>
using namespace std;

class Home {
    friend void MyFriend::visit(const Home &home);

public:
    string livingRoom = "客厅";
private:
    string bedroom = "卧室";
};

如上代码,我们在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 {

};

B.h如下:

#pragma once
#include "A.h"

class B {

};

如上代码,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
    

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

    # 1 "main.cpp"
    # 1 "<built-in>"
    # 1 "<command-line>"
    # 1 "main.cpp"
    
    int main() {
        int max = max(10, 20);
    }
    
    int max(int a, int b) {
        return a > b ? a : b;
    }
    
    
  2. 把C++翻译为汇编语言

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

    这一步是会进行语法检查的,此时就报错了,如下:

    在这里插入图片描述

    如上图所示,提示我们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;
}

math.h文件:

int max(int a, int b);
int min(int a, int b);

main.cpp文件:

#include "math.h"

int main() {
    int max_result = max(10, 20);
    int min_result = min(10, 20);
}

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

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

# 1 "main.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 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);
}

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

min.h文件:

int min(int a, int b);

max.h文件:

#include "min.h"

int max(int a, int b);

main.cpp文件:

#include "max.h"

int main() {
    int max_result = max(10, 20);
    int min_result = min(10, 20);
}

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

# 1 "main.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 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);
}

可以看到,虽然我们在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;
}

min.cpp文件:

int min(int a, int b) {
	return a < b ? a : b;
}

再次执行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

执行效果如下:

在这里插入图片描述

如果有100个cpp文件,这命令写起来也是要命,所以有更简单的命令:

g++ *.cpp -o main.exe

可以看到,一步到位,其实它底层也是分多个步骤来完成编译的,只是这些步骤由编译程序自动完成。这种编译方式不会产生中间文件,也就是我们看不到.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);
}

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

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

# 1 "main.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 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);
}

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

max.h文件如下:

#include "min.h"

int max(int a, int b);

min.h文件如下:

#include "max.h"

int min(int a, int b);

main.cpp文件如下:

#include "max.h"

int main() {
    int max_result = max(10, 20);
    int min_result = min(10, 20);
}

执行预编译命令: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);

min.h文件:

#pragma once
#include "max.h"

int min(int a, int b);

再次执行预编译命令: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);
}

这里已经删掉了一些不必要带#开头的代码,可以看到,这次的函数声明就没有重复出现了,编译时,在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;
};

B.h如下:

#pragma once
#include "A.h"

class B
{
public:
	A * a;
};

main.cpp如下:

#include "A.h"

int main() {
    A a;
	B b;
	a.b = &b;
	b.a = &a;
}

因为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;
}

可以看到,因为头文件中有#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;
};

B.h如下:

#pragma once
#include "A.h"

class A;

class B
{
public:
	A * a;
};

再次编译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;
}

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

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

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

a.h如下:

#pragma once
#include "b.h"

class A {

};

b.h如下:

#pragma once
#include "a.h"

class B {

};

main.cpp如下:

#include "a.h"

int main() {
    A a;
	B b;
}

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

#pragma once
#include "b.h"

class A {

};

如上代码,在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;
}

再回看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;
}

如上代码,两个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;
}

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

int main() {
    int a = 60;
    int & b = a;
    fun(b);
    cout << a << endl;
    return 0;
}

类的成员修饰符和虐函数

成员变量

对于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;
    }
}
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函数
}

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

#pragma once
class Person {
public:
    int field1 = 1;
};
#pragma once
#include "Person.h"
class Student : public Person {
public:
    int field1 = 11;
};
#include <iostream>
#include "Student.h"
using namespace std;

int main() {
    Student stu;
    cout << stu.field1 << endl;
    cout << stu.Person::field1 << endl;
    return 0;
}

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

在这里插入图片描述

如上代码,A类中有三个不同修饰符的成员变量,B继承于A。

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

接下来,我们把B类中的public改为protected,则在main函数中field1也不能访问了,即使使用父类引用也无法访问,如下:

在这里插入图片描述

在子类中,也可以声明同名成员变量,此时的修饰符就可以随便写都可以,因为它没有覆盖的概念,相当于是新的成员变量,如下:

在这里插入图片描述

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

在这里插入图片描述

可以看到,只有访问A中的成员受到了影响。

成员函数

在这里插入图片描述

运行结果如下:

11
1

如上代码,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,示例如下:

在这里插入图片描述
在这里插入图片描述
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门