当前位置: 首页 > news >正文

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的要求,因为它在整个程序中只有一个定义。

相关文章:

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

这个问题是刚了解预编译的时候产生的疑惑。 声明是指向编译器告知某个变量、函数或类的存在及其类型&#xff0c;但并不分配实际的存储空间。声明的主要目的是让编译器知道如何解析程序中的符号引用。定义不仅告诉编译器实体的存在&#xff0c;还会为该实体分配存储空间&#…...

PythonFlask框架

文章目录 处理 Get 请求处理 POST 请求应用 app.route(/tpost, methods[POST]) def testp():json_data request.get_json()if json_data:username json_data.get(username)age json_data.get(age)return jsonify({username: username测试,age: age})从 flask 中导入了 Flask…...

Kotlin开发(六):Kotlin 数据类,密封类与枚举类

引言 想象一下&#xff0c;你是个 Kotlin 开发者&#xff0c;敲着代码忽然发现业务代码中需要一堆冗长的 POJO 类来传递数据。烦得很&#xff1f;别急&#xff0c;Kotlin 贴心的 数据类 能帮你自动生成 equals、hashCode&#xff0c;直接省时省力&#xff01;再想想需要多种状…...

冬天适合养什么鱼?

各位鱼友们&#xff0c;冬天来了&#xff0c;是不是还在为养什么鱼而烦恼&#xff1f;别担心&#xff0c;今天就来给大家好好推荐一些适合冬天养的鱼&#xff0c;让你的水族箱在寒冷的冬天也能生机勃勃&#xff01; 一、金鱼&#xff1a;冬日里的“小暖男” 金鱼绝对是冬季养鱼…...

【C++动态规划 状态压缩】2597. 美丽子集的数目|2033

本文涉及知识点 C动态规划 LeetCode2597. 美丽子集的数目 给你一个由正整数组成的数组 nums 和一个 正 整数 k 。 如果 nums 的子集中&#xff0c;任意两个整数的绝对差均不等于 k &#xff0c;则认为该子数组是一个 美丽 子集。 返回数组 nums 中 非空 且 美丽 的子集数目。…...

前端-Rollup

Rollup 是一个用于 JavaScript 的模块打包工具&#xff0c;它将小的代码片段编译成更大、更复杂的代码&#xff0c;例如库或应用程序。它使用 JavaScript 的 ES6 版本中包含的新标准化代码模块格式&#xff0c;而不是以前的 CommonJS 和 AMD 等特殊解决方案。ES 模块允许你自由…...

20【变量的深度理解】

一说起变量&#xff0c;懂点编程的都知道&#xff0c;但是在理解上可能还不够深 变量就是存储空间&#xff0c;电脑上的存储空间有永久&#xff08;硬盘&#xff09;和临时&#xff08;内存条&#xff09;两种&#xff0c;永久数据重启电脑后依旧存在&#xff0c;临时数据只…...

大数据学习之Kafka消息队列、Spark分布式计算框架一

Kafka消息队列 章节一.kafka入门 4.kafka入门_消息队列两种模式 5.kafka入门_架构相关名词 Kafka 入门 _ 架构相关名词 事件 记录了世界或您的业务中 “ 发生了某事 ” 的事实。在文档中 也称为记录或消息。当您向 Kafka 读取或写入数据时&#xff0c;您以事件的 形式执行…...

基于Flask的旅游系统的设计与实现

【Flask】基于Flask的旅游系统的设计与实现&#xff08;完整系统源码开发笔记详细部署教程&#xff09;✅ 目录 一、项目简介二、项目界面展示三、项目视频展示 一、项目简介 该系统采用Python作为后端开发语言&#xff0c;结合前端Bootstrap框架&#xff0c;为用户提供了丰富…...

“AI视频智能分析系统:让每一帧视频都充满智慧

嘿&#xff0c;大家好&#xff01;今天咱们来聊聊一个特别厉害的东西——AI视频智能分析系统。想象一下&#xff0c;如果你有一个超级聪明的“视频助手”&#xff0c;它不仅能自动识别视频中的各种元素&#xff0c;还能根据内容生成详细的分析报告&#xff0c;是不是感觉特别酷…...

算法随笔_31:移动零

上一篇:算法随笔_30: 去除重复字母-CSDN博客 题目描述如下: 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 请注意 &#xff0c;必须在不复制数组的情况下原地对数组进行操作。 示例 1: 输入: nums [0,1,…...

改进候鸟优化算法之二:基于混沌映射的候鸟优化算法(MBO-CM)

基于混沌映射的候鸟优化算法(Migrating Birds Optimization based on Chaotic Mapping,MBO-CM)是一种结合了混沌映射与候鸟优化算法(Migrating Birds Optimization,MBO)的优化方法。 一、候鸟优化算法(MBO)简介 候鸟优化算法是一种自然启发的元启发式算法,由Duman等人…...

在Docker 容器中安装 Oracle 19c

在 Docker 容器中安装 Oracle 19c 是可行的&#xff0c;但它相较于其他数据库&#xff08;如 MySQL、PostgreSQL 等&#xff09;会复杂一些&#xff0c;因为 Oracle 数据库有一些特定的要求&#xff0c;如操作系统和库的依赖&#xff0c;以及许可证问题。 不过&#xff0c;Ora…...

使用Avalonia UI实现DataGrid

1.Avalonia中的DataGrid的使用 DataGrid 是客户端 UI 中一个非常重要的控件。在 Avalonia 中&#xff0c;DataGrid 是一个独立的包 Avalonia.Controls.DataGrid&#xff0c;因此需要单独通过 NuGet 安装。接下来&#xff0c;将介绍如何安装和使用 DataGrid 控件。 2.安装 Dat…...

MySQL中的读锁与写锁:概念与作用深度剖析

MySQL中的读锁与写锁&#xff1a;概念与作用深度剖析 在MySQL数据库的并发控制机制中&#xff0c;读锁和写锁起着至关重要的作用。它们是确保数据在多用户环境下能够正确、安全地被访问和修改的关键工具。 一、读锁&#xff08;共享锁&#xff09;概念 读锁&#xff0c;也称为…...

Dest1ny漏洞库:用友 U8 Cloud ReleaseRepMngAction SQL 注入漏洞(CNVD-2024-33023)

大家好&#xff0c;今天是Dest1ny漏洞库的专题&#xff01;&#xff01; 会时不时发送新的漏洞资讯&#xff01;&#xff01; 大家多多关注&#xff0c;多多点赞&#xff01;&#xff01;&#xff01; 0x01 产品简介 用友U8 Cloud是用友推出的新一代云ERP&#xff0c;主要聚…...

python学opencv|读取图像(四十九)原理探究:使用cv2.bitwise()系列函数实现图像按位运算

【0】基础定义 按位与运算&#xff1a;两个等长度二进制数上下对齐&#xff0c;全1取1&#xff0c;其余取0。 按位或运算&#xff1a;两个等长度二进制数上下对齐&#xff0c;有1取1&#xff0c;其余取0。 按位异或运算&#xff1a; 两个等长度二进制数上下对齐&#xff0c;相…...

【面试】【编程范式总结】面向对象编程(OOP)、函数式编程(FP)和响应式编程(RP)

一、编程范式总结 编程范式是指开发软件时采用的一种方法论或思维方式&#xff0c;主要包括面向对象编程&#xff08;OOP&#xff09;、**函数式编程&#xff08;FP&#xff09;和响应式编程&#xff08;RP&#xff09;**等。这些范式的不同特性和适用场景&#xff0c;帮助开发…...

创建要素图层和表视图

操作方法: 下面按照步骤学习如何使用Make Feature Layer和Make Table View工具 1.在arcmap中打开活动地图文档 2.导入arcpy模块 3.设置工作空间 arcpy.env.workspace "<>" 4.使用try语句,使用Make Feature Layer工具创建内存副本 try:flayer arcpy.Ma…...

51单片机入门_01_单片机(MCU)概述(使用STC89C52芯片;使用到的硬件及课程安排)

文章目录 1. 什么是单片机1.1 微型计算机的组成1.2 微型计算机的应用形态1.3 单板微型计算机1.4 单片机(MCU)1.4.1 单片机内部结构1.4.2 单片机应用系统的组成 1.5 80C51单片机系列1.5.1 STC公司的51单片机1.5.1 STC公司单片机的命名规则 2. 单片机的特点及应用领域2.1 单片机的…...

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

Linux链表操作全解析

Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表&#xff1f;1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...

docker详细操作--未完待续

docker介绍 docker官网: Docker&#xff1a;加速容器应用程序开发 harbor官网&#xff1a;Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台&#xff0c;用于将应用程序及其依赖项&#xff08;如库、运行时环…...

相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解

【关注我&#xff0c;后续持续新增专题博文&#xff0c;谢谢&#xff01;&#xff01;&#xff01;】 上一篇我们讲了&#xff1a; 这一篇我们开始讲&#xff1a; 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下&#xff1a; 一、场景操作步骤 操作步…...

leetcodeSQL解题:3564. 季节性销售分析

leetcodeSQL解题&#xff1a;3564. 季节性销售分析 题目&#xff1a; 表&#xff1a;sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...

Spring AI与Spring Modulith核心技术解析

Spring AI核心架构解析 Spring AI&#xff08;https://spring.io/projects/spring-ai&#xff09;作为Spring生态中的AI集成框架&#xff0c;其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似&#xff0c;但特别为多语…...

Linux --进程控制

本文从以下五个方面来初步认识进程控制&#xff1a; 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程&#xff0c;创建出来的进程就是子进程&#xff0c;原来的进程为父进程。…...

代理篇12|深入理解 Vite中的Proxy接口代理配置

在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...

mac 安装homebrew (nvm 及git)

mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用&#xff1a; 方法一&#xff1a;使用 Homebrew 安装 Git&#xff08;推荐&#xff09; 步骤如下&#xff1a;打开终端&#xff08;Terminal.app&#xff09; 1.安装 Homebrew…...

C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)

名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、为什么要开发一个日历程序?1. 深入理解时间算法2. 练习面向对象设计3. 学习数据结构应用二、核心算法深度解析…...