🔥博客主页: 我要成为C++领域大神
🎥系列专栏:【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】
❤️感谢大家点赞👍收藏⭐评论✍️
本博客致力于分享知识,欢迎大家共同学习和交流。
头文件中变量和函数的声明
修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”
1. extern修饰变量的声明
如果文件a.cpp需要引用b.cpp中变量int v,就可以在a.cpp中声明extern int v,然后就可以引用变量v。
这里需要注意的是,被引用的变量v的链接属性必须是外链接(external)的,也就是说a.cpp要引用到v,不只是取决于在a.cpp中声明extern int v,还取决于变量v本身是能够被引用到的。
这涉及到c语言的另外一个话题--变量的作用域。能够被其他模块以extern修饰符引用到的变量通常是全局变量。
还有很重要的一点是,extern int v可以放在a.cpp中的任何地方,比如你可以在a.cpp中的函数fun定义的开头处声明extern int v,然后就可以引用到变量v了,只不过这样只能在函数fun作用域中引用v罢了,这还是变量作用域的问题。对于这一点来说,很多人使用的时候都心存顾虑。好像extern声明只能用于文件作用域似的。
#include <stdio.h>
// 函数外定义变量 x 和 y
int x;
int y;
int addtwonum()
{
// 函数内声明变量 x 和 y 为外部变量
extern int x;
extern int y;
// 给外部变量(全局变量)x 和 y 赋值
x = 1;
y = 2;
return x+y;
}
int main()
{
int result;
// 调用函数 addtwonum
result = addtwonum();
printf("result 为: %d",result);
return 0;
}
其中,变量在头部就已经被声明,但是定义与初始化在主函数内
输出的结果是result为:3
2. extern修饰函数声明。
从本质上来讲,变量和函数没有区别。函数名是指向函数二进制块开头处的指针。
如果文件a.cpp需要引用b.cpp中的函数,比如在b.c中原型是int fun(int mu),那么就可以在a.c中声明extern int fun(int mu),然后就能使用fun来做任何事情。
就像变量的声明一样,extern int fun(int mu)可以放在a.cpp中任何地方,而不一定非要放在a.cpp的文件作用域的范围中。
#include <stdio.h>
/*外部变量声明*/
extern int x;
extern int y;
int addtwonum()
{
return x + y;
}
#include <stdio.h>
/*定义两个全局变量*/
int x=1;
int y=2;
extern int addtwonum();
int main(void)
{
int result;
result = addtwonum();
printf("result 为: %d\n",result);
return 0;
}
对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件。使用extern和包含头文件来引用函数有什么区别呢?extern的引用方式比包含头文件要简洁得多!extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。
这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。在大型程序编译过程中,这种差异是非常明显的。
在头文件中声明类
在头文件中将类的属性和类成员函数,以及构造·析构函数 声明出来,在对应的源文件中 进行类成员函数的定义,以及通过定义构造函数 来初始化类成员属性。
在头文件A.h中声明类:
#pragma once
void fun();
class CTest{
public:
int m_a;
static int m_b;
const int m_c;
CTest();
~CTest();
static void funStatic();
void funConst() const;
virtual void funVirtual();
};
在对应的源文件A.cpp中定义类:其中,我们需要注意的是,类中三种不同的函数在定义时,可能需要删除关键字。
static void funStatic(); 静态成员函数 定义时,去掉static关键字
void funConst() const; 常量成员函数,在定义时,保留const关键字
virtual void funVirtual(); 虚函数,在定义时,删除virtual关键字
#include<iostream>
#include"A.h"
int a = 1;
int b = 2;
void fun() {
std::cout << "fun" <<std::endl;
}
//静态成员属性定义:
int CTest::m_b = 3;
//构造函数的定义
CTest::CTest() :m_a(10) ,m_c(3){
std::cout<<"CTest" << std::endl;
}
//析构函数的定义
CTest::~CTest() {
std::cout << "~CTest" << std::endl;
}
//静态函数在定义时,去掉static关键字
void CTest::funStatic() {
std::cout << "funStatic" << std::endl;
}
//常函数在定义时,保留const关键字
void CTest::funConst() const {
std::cout << "funConst" << std::endl;
}
//虚函数在定义时,去掉virtual关键字
void CTest::funVirtual() {
std::cout << "funVirtual" << std::endl;
}
头文件和源文件的区别
区别:
1.头文件:变量的声明,函数的声明,类的定义、类成员的声明 源文件:变量的定义,函数的定义,类中成员属性的定义,成员函数的定义
2.单独的头文件不参与编译,源文件 自上而下进行编译当我们编写一个含有语法错误的头文件后,编译正常通过。因此我们可以得出,单独的头文件不参与编译。
但是当源文件中包含此头文件时:编译无法通过
头文件重复
在vs中,当我们添加头文件时,工具会为我们自动在代码上方添加一段"#pragma once"的代码,意为 仅执行一次 。如果在源文件中,仅仅引入了一次头文件,则可以不进行去重操作。若多次引入,则编译器会提示错误:重定义
头文件:
//A.h
//#pragma once
class A
{
public:
int m_a;
A();
};
//B.h
//#pragma once
#include"A.h"
class B {
public:
A a1;
};
//C.h
//#pragma once
#include"A.h"
class C {
public:
A a2;
};
源文件:
#include "A.h"
A::A() :m_a(1) {};
#include<iostream>
#include"B.h"
#include"C.h"
using namespace std;
int main() {
B b;
C c;
cout << b.a1.m_a << endl;
cout << c.a2.m_a << endl;
return 0;
}
此时我们去编译,无法通过,编译器提示我们重定义。这里我们需要了解编译器执行的原理。我们引入头文件,本质是对代码的展开替换,将对应头文件的代码替换过来。若未进行去重处理,当引入的头文件中含有重复定义的变量时,则会出现重定义。
引入头文件的本质:
那么,我们应该如何避免重定义呢?
1.使用vs工具给我们添加的#pragma once 写法简单,效率略高
2.基于宏的逻辑判断 效率低,宏名字有可能会重复,导致程序逻辑错误
#ifndef 宏名字
#define 宏名字
....代码....
#enddef //宏名字
意为,若在#enddef后面的代码中重复定义相同的宏,则不会执行后面代码中#ifndef #enddef里的内容
#ifndef __A_h__
#define __A_h__
class A
{
public:
int m_a;
A();
};
#endif
编译可以正常通过。
重复部分的代码变成了灰色,被忽略了。
告诉编译器当前的这个头文件在其他的源文件中只会包含一次
#pragma once :写法简单,效率略高