为什么不推荐在头文件中直接定义函数?

news2024/11/25 0:39:50

1. 函数的分文件编写

在C++中,函数的分文件编写是一种让代码结构更加清晰的方法,通常可以分为以下几个步骤:

  • 创建后缀名为 .h 的头文件,在头文件中写函数的声明,以及可能用到的其他头文件或命名空间
  • 创建后缀名为 .cpp 的源文件,在源文件中写函数的定义,同时引入自定义头文件,将头文件与源文件绑定
  • 在需要使用函数的地方,引入自定义头文件,然后直接调用函数,无需再写函数的实现

例如,如果要编写一个求两个数最大值的函数,可以这样做:

  • 创建一个 max.h 头文件,在其中写入以下内容:
#pragma once // 防止头文件重复包含
#include <iostream> // 引入输入输出流头文件
using namespace std; // 使用标准命名空间
// 函数声明
int max(int a, int b);
  • 创建一个 max.cpp 源文件,在其中写入以下内容:
#include "max.h" // 引入自定义头文件
// 函数定义
int max(int a, int b) {
    return a > b ? a : b; // 三目运算符,返回最大值
}
  • 在需要使用函数的地方,例如 main.cpp 文件中,引入自定义头文件,并调用函数:
#include "max.h" // 引入自定义头文件
int main() {
    int a = 10;
    int b = 20;
    cout << "The max of " << a << " and " << b << " is " << max(a, b) << endl; // 调用函数并输出结果
    system("pause"); // 暂停程序
    return 0;
}

文件结构如图所示:

img

2. 头文件中不推荐直接定义函数

在头文件中直接写函数的定义是不推荐的,有以下几个原因:

  • 在头文件中写函数的定义会导致重复定义的错误,如果这个头文件被多个源文件包含。因为每个源文件都会把头文件的内容复制过来,相当于在多个地方定义了同一个函数,这违反了单定义原则
  • 在头文件中写函数的定义会增加编译的时间,如果这个头文件被频繁修改。因为每次修改头文件后,所有包含这个头文件的源文件都需要重新编译,这对于大型项目来说非常耗时
  • 在头文件中写函数的定义会降低代码的可读性和可维护性,如果这个头文件包含了很多函数的定义。因为头文件的主要作用是提供函数的声明和接口,而不是实现细节。把函数的定义放在源文件中,可以让代码结构更清晰,也便于隐藏实现细节和保护数据

2.1 单定义原则

在头文件中写函数的定义会导致重复定义的错误,如果这个头文件被多个源文件包含。比如,假设有一个头文件 max.h,其中定义了一个求两个数最大值的函数:

// max.h
#pragma once
#include <iostream>
using namespace std;

int max(int a, int b) {
    return a > b ? a : b;
}

然后,有两个源文件 main1.cppmain2.cpp,都包含了这个头文件,并且都调用了这个函数:

// main1.cpp
#include "max.h"

int foo() {
    cout << "The max of 10 and 20 is " << max(10, 20) << endl;
    return 0;
}
// main2.cpp
#include "max.h"

int main() {
    cout << "The max of 30 and 40 is " << max(30, 40) << endl;
    return 0;
}

img

看到这里可能会有个疑问,编译的时候 main1.cpp 调用 max.h 中的函数,但是 main2.cpp 中的主函数中没有调用 main1.cpp 中的函数,为什么还是会编译不通过呢?两个不同的文件定义同一个函数也会冲突吗?即使其中一个文件和另一个文件没有任何关系?

编译时,每个源文件会生成一个目标文件,然后链接生成可执行文件。即使 main2.cpp 没有调用 main1.cpp 的函数,但 main1.cpp 中包含了 max.h,相当于在 main1.cpp 中定义了max函数,与 main2.cpp 中的max函数冲突。当链接时,如果出现同名的函数,就会出现重复定义的错误。因此,每个函数应该只在一个源文件中定义,或者使用命名空间或静态修饰符来避免冲突


为了解决这个问题,我们应该把函数的定义放在另一个源文件 max.cpp 中,然后在头文件中只声明函数:

// max.h
#pragma once
#include <iostream>
using namespace std;

int max(int a, int b); // 函数声明
// max.cpp
#include "max.h"
int max(int a, int b) { // 函数定义
    return a > b ? a : b;
}

img

这样就可以避免重复定义的错误了


2.2 减少编译时间

在头文件中写函数的定义会增加编译的时间,如果这个头文件被频繁修改。比如,假设有一个头文件 math.h,其中定义了一些数学相关的函数:

// math.h
double sin(double x) {
    // some code to calculate sin(x)
}

double cos(double x) {
    // some code to calculate cos(x)
}

double tan(double x) {
    // some code to calculate tan(x)
}

然后,有很多源文件都包含了这个头文件,并且都调用了这些函数。如果我们想要修改或添加某个函数的实现细节,比如改进 sin 函数的算法,那么我们就需要修改头文件 math.h。但是这样一来,所有包含了这个头文件的源文件都需要重新编译,因为它们都依赖于头文件的内容。这对于大型项目来说非常耗时。为了解决这个问题,我们应该把函数的定义放在另一个源文件 math.cpp 中,然后在头文件中只声明函数:

// math.h
double sin(double x); // 函数声明
double cos(double x); // 函数声明
double tan(double x); // 函数声明
// math.cpp
#include "math.h"
double sin(double x) { // 函数定义
    // some code to calculate sin(x)
}

double cos(double x) { // 函数定义
    // some code to calculate cos(x)
}

double tan(double x) { // 函数定义
    // some code to calculate tan(x)
}

这样就可以减少编译的时间了,因为只有修改或添加了函数的源文件才需要重新编译

简单来说,分为两种情况

  • 第一种:在头文件中定义函数。如果有很多源文件都引用了这个头文件,那么当头文件修改后,所有引用头文件的源文件都要重新编译,对于大型项目非常耗时

  • 第二种:把函数的定义和声明放在不同的文件中。这样做可以使得当源文件中定义的函数发生修改时,只需要重新编译被修改的源文件就可以了,不需要所有引用这个头文件的源文件重新编译,节省了非常多的时间


为什么在头文件中定义的函数发生改变时,所有包含该头文件的源文件需要重新编译?

还是借用以上的例子,我的猜想是这样的

假如在 main.cpp 源文件中引用 math.h 头文件,相当于把头文件中的内容复制到了源文件里

那么如果 math.h 头文件中定义函数,并且 main.cpp 源文件中引用了 math.h 头文件,则相当于把 math.h 中的定义的函数复制到 main.cpp 源文件里,一旦头文件中的函数发生改变,那么就相当于源文件发生了改变

因此所有包含 math.h 头文件的源文件都需要重新编译

此外,多个源文件包含同一个定义函数的头文件,会导致重定义的错误。这里只是举个例子假设编译器允许这样的操作,实际上编译不会通过

img

调用函数时的索引顺序:

在源文件中调用函数的时候,是先到头文件里找声明的函数,然后再通过链接的过程找到对应的源文件里的函数

如下图所示,main.cpp 调用函数时,先到 math.h 中找到声明的函数,然后再通过链接的过程找到对应的源文件 math.cpp 里的函数

img

这个过程可以看作是查字典,头文件相当于目录,对应着每个函数所在的位置


2.3 可读性与安全性

在头文件中写函数的定义会降低代码的可读性和可维护性,如果这个头文件包含了很多函数的定义。比如,假设有一个头文件 utils.h,其中定义了一些工具类的函数:

// utils.h
#include <string>
#include <vector>
using namespace std;

string trim(string s) {
    // some code to trim the whitespace of s
}

vector<string> split(string s, char delim) {
    // some code to split s by delim
}

string join(vector<string> v, char delim) {
    // some code to join v by delim
}

bool is_number(string s) {
    // some code to check if s is a number
}

int to_int(string s) {
    // some code to convert s to int
}

string to_string(int x) {
    // some code to convert x to string
}

这个头文件包含了很多函数的定义,这会让代码看起来很冗长,也不容易找到想要的函数。而且,如果我们想要修改或添加某个函数的实现细节,比如改进 trim 函数的效率,那么我们就需要修改头文件 utils.h。但是这样会影响到所有包含了这个头文件的源文件,也会增加代码的复杂度和出错的风险。为了解决这个问题,我们应该把函数的定义放在另一个源文件 utils.cpp 中,然后在头文件中只声明函数:

// utils.h
#include <string>
#include <vector>
using namespace std;

string trim(string s); // 函数声明
vector<string> split(string s, char delim); // 函数声明
string join(vector<string> v, char delim); // 函数声明
bool is_number(string s); // 函数声明
int to_int(string s); // 函数声明
string to_string(int x); // 函数声明
// utils.cpp
#include "utils.h"

string trim(string s) {
    // some code to trim the whitespace of s
}

vector<string> split(string s, char delim) {
    // some code to split s by delim
}

string join(vector<string> v, char delim) {
    // some code to join v by delim
}

bool is_number(string s) {
    // some code to check if s is a number
}

int to_int(string s) {
    // some code to convert s to int
}

string to_string(int x) {
    // some code to convert x to string
}

ctor<string> v, char delim) {
    // some code to join v by delim
}

bool is_number(string s) {
    // some code to check if s is a number
}

int to_int(string s) {
    // some code to convert s to int
}

string to_string(int x) {
    // some code to convert x to string
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/760742.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ES6迭代器、Set、Map集合和async异步函数

目录 迭代器 Iterator 的作用 Iterator 的遍历过程 Set Map集合 map和对象区别? async异步函数 迭代器 迭代器&#xff08;Iterator&#xff09;就是这样一种机制。它是一种接口&#xff0c;为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口&…

自动化运维工具——Ansible学习(二)

目录 一、handlers和notify结合使用触发条件 1.新建httpd.yml文件 2.复制配置文件到ansible的files目录中 3.卸载被控机已安装的httpd 4.执行httpd.yml脚本 5.更改httpd.conf配置文件 6.使用handlers 7.重新执行httpd.yml脚本 8.检查被控机的端口号是否改变 9.handle…

矩阵乘法的硬件加速

矩阵乘法的硬件加速 这里的硬件加速是指&#xff0c;如果依靠算法&#xff0c;可以通过减少访存次数来加速。可以将数据预取到cache来减少访存次数。 矩阵相乘最简单的实现 寄存器平铺矩阵乘法 将矩阵划分成多个小的矩阵块&#xff0c;小的矩阵块可以存放在寄存器中&#xff0…

QT开发技巧之嵌入式linux QT的QCombobox显示空白的问题

1.问题 开发平台&#xff1a;imx6ull qt版本&#xff1a;5.12.9 在嵌入式linux上运行的qt&#xff0c;QCombobox显示空白&#xff0c;不能显示其中的文本内容 2.解决办法 选中QCombobox&#xff0c;在属性栏中将foucsPolicy由WheelFocus改成NoFocus就好了

用户、角色、权限、菜单--数据库设计

用户角色关联表--user_role id-------------------主键 user_id------------用户ID role_id-------------角色ID create_time------创建时间 is_deleted--------状态&#xff08;0&#xff1a;未删除 1&#xff1a;删除&#xff09; 角色权限关联表--role_permission id------…

【PHP面试题50】Redis的主从复制实现原理是怎么样的?如何保证数据一致性?数据延迟又该如何处理?

文章目录 一、前言二、主从复制的基本原理三、数据一致性的保证四、数据延迟的处理四、总结 一、前言 本文已收录于PHP全栈系列专栏&#xff1a;PHP面试专区。 计划将全覆盖PHP开发领域所有的面试题&#xff0c;对标资深工程师/架构师序列&#xff0c;欢迎大家提前关注锁定。 R…

OpenCv算子

目录 一、索贝尔(sobel)算子 二、沙尔(Scharr)算子 三、拉普拉斯算子 一、索贝尔(sobel)算子 边缘的定义: 边缘是像素值发生跃迁的位置&#xff0c;是图像的显著特征之一&#xff0c;在图像特征提取、对象检测、模式识别等方面都有重要的作用。 sobel算子对图像求一阶导数。…

【V8】【1. 内存布局、隐藏类Hidden Class】

JavaScript 中的对象是由一组组属性和值的集合。JavaScript 对象像一个字典&#xff0c;字符串作为键名&#xff0c;任意对象可以作为键值&#xff0c;可以通过键名读写键值。 在 ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列&#xff0c;字符串属性根据创建时…

python将dataframe数据导入MongoDB非关系型数据库

文章目录 pymongo连接新建数据库和集合pandas导入数据插入数据数据查看 pymongo连接 import pymongo client pymongo.MongoClient("mongodb://localhost:27017/") dblist client.list_database_names() for db in dblist:print(db) #查看已有数据库admin bilibil…

apply()函数--Pandas

1. DataFrame.apply()函数 1.1 函数功能 对DataFrame沿着指定轴运用函数。 函数接收到的对象是Series对象&#xff0c;它们的索引可以是DataFrame的行索引&#xff08;axis0&#xff09;或列索引&#xff08;axis1&#xff09;。结果为Series或DataFrame。 1.2 函数语法 Da…

java基础浮点类型

目录 1&#xff1a;float类型和double类型的定义 2&#xff1a;二者的范围和精度 3&#xff1a;float类型详解 3.1&#xff1a;整数位10进制转换为2进制 3.2&#xff1a;小数位10进制转换为2进制 3.3&#xff1a;把二进制放到浮点类型中 3.4&#xff1a;精度损失 4&…

手动下载composer项目放在vendor目录下并加载

比如添加easywechat。 说是手动&#xff0c;其实半手动。 到GitHub或gitee下载 1、下载后放在项目根目录下的vendor文件夹 2、在项目根目录的文件composer.json文件添加一段 "autoload": {"psr-4": {"EasyWeChat\\": "vendor/overtrue/wech…

9.带你入门matlab假设检验(matlab程序)

1.简述 函数 ztest 格式 h ztest(x,m,sigma) % x为正态总体的样本&#xff0c;m为均值μ0&#xff0c;sigma为标准差&#xff0c;显著性水平为0.05(默认值) h ztest(x,m,sigma,alpha) %显著性水平为alpha [h,sig,ci,zval] ztest(x,m,sigma,alpha,tail) %sig为观察…

3.5 Bootstrap 输入框组

文章目录 Bootstrap 输入框组基本的输入框组输入框组的大小复选框和单选插件按钮插件带有下拉菜单的按钮分割的下拉菜单按钮 Bootstrap 输入框组 本章将讲解 Bootstrap 支持的另一个特性&#xff0c;输入框组。输入框组扩展自 表单控件。使用输入框组&#xff0c;您可以很容易地…

SpringCloud Alibaba——Ribbon底层怎样实现不同服务的不同配置

目录 一、Ribbon底层怎样实现不同服务的不同配置二、源码角度分析 一、Ribbon底层怎样实现不同服务的不同配置 为不同服务创建不同的spring上下文&#xff0c;不同的spring上下文中存放对应这个服务所有的配置。 二、源码角度分析 SpringClientFactory中可以获取到所有ribbon…

自动化运维工具——Ansible学习(一)

目录 一、运维自动化发展历程及技术应用 (二)程序发布相关知识 (三)常用的自动化运维工具 二、Ansible入门 (一)Ansible发展史 (二)特点 (三)Ansible架构 (四)工作原理 (五)Ansible主要组成部分 (六) 安装步骤 1.各种安装方法与命令 (1)rpm包安装&#xff1a;EPEL源…

一篇文章让你学会Elasticsearch中的查询

还是惯例&#xff0c;开头先放章节目录&#xff0c;如果有帮到你的地方&#xff0c;欢迎点赞关注转发&#xff0c;如有错误&#xff0c;欢迎指出&#xff0c;不胜感激 一、环境初始化 version: 3.8 services:cerebro:image: lmenezes/cerebro:0.8.3container_name: cerebroport…

ylb-支付服务pay

总览&#xff1a; 在pay模块util包下&#xff0c;创建签名工具类Pkipair和http工具类HttpUtil&#xff1a; package com.bjpowernode.util;import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.securi…

centos7 docker 安装sql server 2019

contos7安装sql server docker最低1.8或更高 卸载旧的docker sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine 装docker依赖包 #安装所需资源包 sudo yum install -…

mac 下 geoserver 安装

一、去官网下载geoserver https://geoserver.org/ 选择一个版本&#xff0c;然后点进去 二、需要配置java环境和设置geoserver 环境变量 1&#xff09;、java 环境安装 Java Downloads | Oracle 中国 2&#xff09;、环境变量设置 1.打开终端&#xff1a;command 空格键 2…