相对于java,C++中的那些神奇语法
空指针还可以调用成员函数
#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;
}
友元
这个也还好,但是后面会遇到问题,所以也记录一下。
全局函数做友元
示例代码如下:
#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
这个类。
我们把Home
和MyFriend
的定义换一下位置:
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
中使用到了MyFriend
的visit
函数,这是不行的,因为声明MyFriend
只能表明有这样一个类,但是无法知道这个类里面有什么的,于是想到能否把MyFriend
的visit
函数也声明一下呢?如下:
如上图,是不是写法有问题,再如下面:
如上图,还是有问题,那我把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
时,发现这个函数之前没有定义过,所以编译不给通过,分步编译,如下:
-
预编译
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; }
-
把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.h
中include
了min.h
,main.cpp
中include
了max.h
。注意,此时我们的max.h
和min.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
中只是include
了max.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
,这里没有报错,我们只是声明了max
和min
函数,并没有对应的函数定义,这说明在把C语言翻译为汇编的这一步,只检查语法是否正确,但是函数没定义它是不管的,只要有声明即可,接下来再执行第三步编译,把没汇编语言翻译为机器语言g++ -c main.s -o main.o
,跟上一步一样,也没报错,接下来再执行最后一步编译,链接动态库并生成可执行文件:g++ main.o -o main.exe
,这里就报错了,因为要生成最终可执行文件了,然后我们的max
和min
函数并没有定义,所以报错了,如下:
我们把这两个函数的定义补上,如下:
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.cpp
、max.cpp
、min.cppp
分别编译为一对应的main.o
、max.o
、min.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.h
和B.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.h
和B.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
文件即可知道,这是合法的,确实不会有问题。