#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;
}
这个不算奇怪,一种限制成员变量修改的语法,在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这个类。
我们把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中就报错了,如下:
这是因为两个类不能互相包含,详情可看后面的知识
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中,我们只需要包含其中一个头文件即可。
在执行第二步编译的时候报错了,如下:
这里有两个错误提示,第一个提示说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文件即可知道,这是合法的,确实不会有问题。
在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 & a和const 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,则父类中的public和protected会被降级为private,示例如下: