c++面试:类定义为什么可以放到头文件中

这个问题是刚了解预编译的时候产生的疑惑。

  1. 声明是指向编译器告知某个变量、函数或类的存在及其类型,但并不分配实际的存储空间。声明的主要目的是让编译器知道如何解析程序中的符号引用。
  2. 定义不仅告诉编译器实体的存在,还会为该实体分配存储空间(对于变量)或者提供具体的实现(对于函数)。定义只能出现一次,以避免重复定义错误。
  3. 类定义描述了一个类型(或称“蓝图”),它定义了该类型的成员变量、成员函数以及其他特性。类定义本身并不分配内存,它只是提供了创建对象的模板。【很明显与函数定义和变量定义很不相同】对象定义则是基于某个类创建的具体实例,它会在内存中分配空间。
  4. 编译器将类中声明的函数视为内联函数。当你在类定义内部定义成员函数时(即直接在类体内部提供函数体),这些函数默认为是inline的,如果使用 inline ,则意味着编译器会在调用此函数的地方把函数的目标代码直接插入,而不是放置一个真正的函数调用,实际作用就是这个函数事实上已经不再存在,而是像宏一样被就地展开了,因此不存在重复定义的问题。

总而言之,类定义其实只是描述了一个类型,并不会分配具体的空间,在类内实现的函数编译器认为是inline,调用时会展开成具体的代码,所以不存在重复定义。因此类定义可以放到头文件中被不同的源文件引用也不会产生重复定义的问题。

两次导入头文件

预编译指令是以井号(#)开头的指令,它们在编译器进行编译之前执行。预编译指令不是C++语句,因此它们不以分号(;)结尾。预编译指令包括但不限于以下几种:

  • #include:用于包含头文件,将头文件的内容插入到源文件中。系统提供的头文件使用尖括号<>括起来,而用户自定义的头文件使用双引号""括起来。
  • #define:用于定义宏,宏可以是简单的符号常量或带参数的宏。宏定义在预处理阶段会被替换成相应的文本。
  • #if、#ifdef、#ifndef、#else、#elif、#endif:这些条件编译指令用于根据条件判断是否编译某部分代码。
// add.h
int add(int, int);
class Person{
public:int age;
}// add.cpp
#include "add.h"
#include "add.h"
int add(int a, int b) {return a + b;
}

我们使用g++ -E add.cpp -o add.i的命令可以得到预编译后的文件

// 遇见#include "add.h",将add.h中的内容展开// 第一次执行#include "add.h"展开成如下内容
int add(int, int);
class Person{
public:int age;
}// 第二次执行#include "add.h"展开成如下内容
int add(int, int);
class Person{
public:int age;
}int add(int, int);
class Person{
public:int age;
}int add(int a, int b) {return a + b;
}

可以看到Person这个类被定义了两次,很明显是不合理的,我们可以通过#ifndef来解决。

// add.h
#ifndef __ADD_H // 如果没有定义过__ADD_H这个宏
#define __ADD_H // 那么就定义这个宏int add(int, int);
class Person{
public:int age;
}#endif

第一次引入add.h的时候还没有定义过__ADD_H,那么就定义这个宏以及头文件里的内容,第二次引入add.h的时候,已经定义过__ADD_H了,那么就不将内容替换进去。对于cpp而言,还可以使用#pragma once

#pragma once
int add(int, int);
class Person{
public:int age;
}

定义和声明

在C++中,定义和声明是两个不同的概念,它们各自有着明确的用途和含义。理解这两者的区别对于编写正确且高效的C++代码至关重要。

  • 声明是指向编译器告知某个变量、函数或类的存在及其类型,但并不分配实际的存储空间。声明的主要目的是让编译器知道如何解析程序中的符号引用。例如:
extern int a; // 声明一个名为a的整型变量,但不分配内存
int add(int x, int y); // 声明一个名为add的函数,但不提供实现
class MyClass; // 前置声明,仅声明了MyClass的存在

声明允许你在代码的一个部分提到某个实体,并在另一个部分提供其实现或定义。这对于模块化编程特别有用,因为它使得你可以将接口与实现分离。

  • 定义不仅告诉编译器实体的存在,还会为该实体分配存储空间(对于变量)或者提供具体的实现(对于函数)。定义只能出现一次,以避免重复定义错误。
int a = 10; // 定义了一个名为a的整型变量,并初始化为10int add(int x, int y) {return x + y;
} // 定义并实现了add函数void MyClass::myFunction() {// 函数实现
}

一个实体只能有一个定义(遵循“一个定义原则”,ODR),但在多个源文件中可以进行声明。并且定义会导致为变量分配存储空间或为函数生成机器码,而声明不会。

类为什么可以放到头文件中

类或者结构体只是描述了对数据的组织方式,并不需要申请空间,所以是声明 ,因此可以在多个文件中引用。

  • 类定义:类定义描述了一个类型(或称“蓝图”),它定义了该类型的成员变量、成员函数以及其他特性。类定义本身并不分配内存,它只是提供了创建对象的模板。
  • 对象定义:对象定义则是基于某个类创建的具体实例,它会在内存中分配空间。

通常我们会将类定义在头文件中,在.cpp文件中实现方法,cpp中的类函数才会生成字节码,才是真实的类方法实现(定义),而.h中的类函数则相当于只是一个【接口声明】,不会生成真正的代码。

编译器将类中声明的函数视为内联函数。因此,当您调用这个类函数时,有两个选项:

  1. 默认视为内联:当你在类定义内部定义成员函数时(即直接在类体内部提供函数体),这些函数默认为是inline的,如果使用 inline ,则意味着编译器会在调用此函数的地方把函数的目标代码直接插入,而不是放置一个真正的函数调用,实际作用就是这个函数事实上已经不再存在,而是像宏一样被就地展开了,因此不存在重复定义的问题。
  2. 弱符号:在某些情况下,如果编译器决定不对某个inline函数进行内联,它会将该函数作为“弱符号”处理。这意味着,尽管该函数在多个翻译单元中有定义,但在链接阶段只会保留一个副本。链接器会选择其中一个定义作为最终使用的版本,而忽略其他重复的定义。这确保了即使有多个定义存在,也不会导致链接错误。

假设我们有一个简单的类定义如下:

// MyClass.h
#pragma onceclass MyClass {
public:void inlineFunction() { std::cout << "Inline function called" << std::endl; } // 内联函数void nonInlineFunction(); // 声明非内联函数
};

然后在对应的.cpp文件中定义非内联成员函数:

// MyClass.cpp
#include "MyClass.h"
#include <iostream>void MyClass::nonInlineFunction() { // 定义非内联函数std::cout << "Non-inline function called" << std::endl;
}

在另一个源文件中使用这个类:

// main.cpp
#include "MyClass.h"int main() {MyClass obj;obj.inlineFunction();obj.nonInlineFunction();return 0;
}

在这个例子中:

  • inlineFunction 是在类定义内部定义的,因此它被视为inline函数。即使在多个源文件中包含MyClass.h,也不会违反ODR,因为这些定义是相同的。
  • nonInlineFunction 的定义只存在于MyClass.cpp中,符合ODR的要求,因为它在整个程序中只有一个定义。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RAG是否被取代(缓存增强生成-CAG)吗?

引言&#xff1a; 本文深入研究一种名为缓存增强生成&#xff08;CAG&#xff09;的新技术如何工作并减少/消除检索增强生成&#xff08;RAG&#xff09;弱点和瓶颈。 LLMs 可以根据输入给他的信息给出对应的输出&#xff0c;但是这样的工作方式很快就不能满足应用的需要: 因…

TCP三次握手和四次挥手

TCP 三次握手和四次挥手 TCP&#xff08;传输控制协议&#xff09;是一种面向连接的协议&#xff0c;在建立连接和断开连接时分别需要通过 三次握手 和 四次挥手 来确保通信的可靠性和完整性。 1. 三次握手 三次握手是 TCP 建立连接的过程&#xff0c;确保客户端和服务器双方…

在线免费快速无痕去除照片海报中的文字logo

上期和大家分享了用photoshop快速无痕去除照片海报中的文字logo的方法&#xff0c;有的同学觉得安装PS太麻烦&#xff0c;有那下载安装时间早都日落西山了&#xff0c;问有没有合适的在线方法可以快速去除&#xff1b;达芬奇上网也尝试了几个网站&#xff0c;今天分享一个对国人…

VS C++ 配置OPENCV环境

VS C 配置OPENCV环境 1.下载opencv2.安装环境3.opencv环境4.VS配置opencv环境5.EXE执行文件路径的环境lib和dll需要根据是debug还是release环境来区分使用哪个 6.Windows环境 1.下载opencv 链接: link 2.安装环境 双击运行即可 3.opencv环境 include文件路径:opencv\build\…

excel如何查找一个表的数据在另外一个表是否存在

比如“Sheet1”有“张三”、“李四”“王五”三个人的数据&#xff0c;“Sheet2”只有“张三”、“李四”的数据。我们通过修改“Sheet1”的“民族”或者其他空的列&#xff0c;修改为“Sheet2”的某一列。这样修改后筛选这个修改的列为空的或者为出错的&#xff0c;就能找到两…

电路研究9.2.2——合宙Air780EP分组域相关命令

这个好像是GPRS网络相关的&#xff0c;我过来研究一下。 8.1GPRS 网络注册状态&#xff1a;ATCGREG 设置指令控制关于GPRS注册状态非请求结果码的显示。 当<n>1 并且 MT 的 GPRS 注册状态发生改变&#xff0c;即会有CGREG:<stat>的 URC 上报。 当 <n>2 并 且…

DeepSeek R1:中国AI黑马的崛起与挑战

文章目录 技术突破&#xff1a;从零开始的推理能力进化DeepSeek R1-Zero&#xff1a;纯RL训练的“自我觉醒”DeepSeek R1&#xff1a;冷启动与多阶段训练的平衡之道 实验验证&#xff1a;推理能力的全方位跃升基准测试&#xff1a;超越顶尖闭源模型蒸馏技术&#xff1a;小模型的…

UiAutomator的详细介绍

UIAutomator作为一种高效的测试框架&#xff0c;通过自动化手段显著提升了用户界面&#xff08;UI&#xff09;测试的效率与准确性。它不仅支持自动生成功能测试用例&#xff0c;还允许开发者在不同设备上执行这些测试&#xff0c;确保了应用程序的一致性和稳定性。 以下是对 …

开源物业管理系统赋能社区管理提升居民服务体验与满意度

内容概要 在现代物业管理中&#xff0c;开源物业管理系统的出现为社区管理带来了新的契机。这种系统的核心思想是通过开放、共享的方式&#xff0c;为各类物业管理需求提供灵活的解决方案。从基本的信息传递到复杂的投诉处理&#xff0c;开源物业管理系统能够根据不同社区的实…

【深入理解FFMPEG】命令行阅读笔记

这里写自定义目录标题 第三章 FFmpeg工具使用基础3.1 ffmpeg常用命令3.1.13.1.3 转码流程 3.2 ffprobe 常用命令3.2.1 ffprobe常用参数3.2.2 ffprobe 使用示例 3.3 ffplay常用命令3.3.1 ffplay常用参数3.3.2 ffplay高级参数3.3.4 ffplay快捷键 第4章 封装与解封装4.1 视频文件转…

递归搜索回溯综合练习(十五题)

目录 1.找出所有子集的异或总和再求和 2.全排列2 3.电话号码的字母组合 4.括号生成 5.组合 6.目标和 1.path作为全局变量 2.path用于传参 7.组合总和 方法一&#xff1a;按照每个空选什么数字进行递归 方法二&#xff1a;按照每个数字选几个进行递归 8.字母大小写全排…

JWT实现单点登录

文章目录 JWT实现单点登录JWT 简介存在问题及解决方案登录流程后端程序实现前端保存Tokenstore存放信息的缺点及解决 校验流程&#xff1a;为gateway增加登录校验拦截器 另一种单点登录方法&#xff1a;Token&#xff0b;Redis实现单点登录 JWT实现单点登录 登录流程&#xff…

qt-QtQuick笔记之常见项目类简要介绍

qt-QtQuick笔记之常见项目类简要介绍 code review! 文章目录 qt-QtQuick笔记之常见项目类简要介绍1.QQuickItem2.QQuickRectangle3.QQuickImage4.QQuickText5.QQuickBorderImage6.QQuickTextInput7.QQuickButton8.QQuickSwitch9.QQuickListView10.QQuickGridView11.QQuickPopu…

循环神经网络(RNN)+pytorch实现情感分析

目录 一、背景引入 二、网络介绍 2.1 输入层 2.2 循环层 2.3 输出层 2.4 举例 2.5 深层网络 三、网络的训练 3.1 训练过程举例 1&#xff09;输出层 2&#xff09;循环层 3.2 BPTT 算法 1&#xff09;输出层 2&#xff09;循环层 3&#xff09;算法流程 四、循…

Autosar-Os是怎么运行的?(多核系统运行)

写在前面&#xff1a; 入行一段时间了&#xff0c;基于个人理解整理一些东西&#xff0c;如有错误&#xff0c;欢迎各位大佬评论区指正&#xff01;&#xff01;&#xff01; 目录 1.Autosar多核操作系统 1.1多核启动过程 1.2多核运行过程 1.2.1核间任务同步 1.2.2Counte…

【C语言练习题】正弦函数

题目&#xff1a; 根据麦克劳林公式计算正弦值。 输入格式 x ε 注&#xff1a;x 为角(弧度)&#xff0c;ε 为计算精度。 输出格式 y 注&#xff1a;y 为 x 的正弦值&#xff0c;输出 6 位小数。 输入样例1 0.5235987755982989 0.00000001输出样例1 0.500000输入样例2 314.68…

GBase 8a 9.5.3.27 DBlink配置---源端GBase

原理图 1.目标端集群将数据请求由gcluster的5258端口发送至dblink的9898端口 2.Dblink将请求由9898端口转发至源端集群的5258端口 3.源端数据库将接收的请求生成执行计划&#xff0c;由gcluster的5258端口下发至各gnode的5050端口 4.源端的5050端口接收到执行计划进行查询&…

二次封装的方法

二次封装 我们开发中经常需要封装一些第三方组件&#xff0c;那么父组件应该怎么传值&#xff0c;怎么调用封装好的组件原有的属性、插槽、方法&#xff0c;一个个调用虽然可行&#xff0c;但十分麻烦&#xff0c;我们一起来看更简便的方法。 二次封装组件&#xff0c;属性怎…

*胡闹厨房*

前期准备 详细教程 一、创建项目 1、选择Universal 3D,创建项目 2、删除预制文件Readme:点击Remove Readme Assets,弹出框上点击Proceed 3、Edit-Project Setting-Quality,只保留High Fidelity 4、打开 Assets-Settings ,保留URP-HighFidelity-Renderer 和 URP-High…

Effective Objective-C 2.0 读书笔记—— objc_msgSend

Effective Objective-C 2.0 读书笔记—— objc_msgSend 文章目录 Effective Objective-C 2.0 读书笔记—— objc_msgSend引入——静态绑定和动态绑定OC之中动态绑定的实现方法签名方法列表 其他方法objc_msgSend_stretobjc_msgSend_fpretobjc_msgSendSuper 尾调用优化总结参考文…