当前位置: 首页 > 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 单片机的…...

万物皆有联系:驼鸟和布什

布什&#xff1f;一块布十块钱吗&#xff1f;不是&#xff0c;大家都知道&#xff0c;美国有两个总统&#xff0c;叫老布什和小布什&#xff0c;因为两个布什总统&#xff08;父子俩&#xff09;&#xff0c;大家就这么叫来着&#xff0c;目的是为了好区分。 布什总统的布什&a…...

【最后203篇系列】007 使用APS搭建本地定时任务

说明 最大的好处是方便。 其实所有任务的源头&#xff0c;应该都是通过定时的方式&#xff0c;在每个时隙发起轮询。当然在任务的后续传递中&#xff0c;可以通过CallBack或者WebHook的方式&#xff0c;以事件的形态进行。这样可以避免长任务执行的过程中进行等待和轮询。 总结…...

go gin配置air

一、依赖下载 安装最新&#xff0c;且在你工作区下进行安装&#xff0c;我的是D:/GO是我的工作区&#xff0c;所有项目都在目录下的src&#xff0c; go install github.com/air-verse/airlatest 如果出现类似报错&#xff1a; 将图中第三行 github.com/air-verse/air 替换最…...

Java定时任务实现方案(五)——时间轮

时间轮 这篇笔记&#xff0c;我们要来介绍实现Java定时任务的第五个方案&#xff0c;使用时间轮&#xff0c;以及该方案的优点和缺点。 ​ 时间轮是一种高效的定时任务调度算法&#xff0c;特别适用于大量定时任务的场景。时间轮的定时任务实现&#xff0c;可以使用DelayQueue…...

【事务管理】

目录 一. 介绍与操作二. Spring事务管理三. 事务四大特性 \quad 一. 介绍与操作 \quad \quad 二. Spring事务管理 \quad 推荐加在经常进行增删改的方法上 \quad 三. 事务四大特性 \quad ctrlaltt...

Highcharts 柱形图:深入解析与最佳实践

Highcharts 柱形图:深入解析与最佳实践 引言 Highcharts 是一个功能强大的图表库,它允许用户轻松地在网页上创建各种类型的图表。其中,柱形图因其直观的展示方式,在数据分析、业务报告等领域得到了广泛应用。本文将深入解析 Highcharts 柱形图,包括其基本用法、高级特性…...

js笔记(黑马程序员)

js&#xff08;day2&#xff09; 一、运算符 1.赋值运算符 运算符作用加法赋值-减法赋值*乘法复制/除法赋值%取余赋值 2.一元运算符 符号作用说明自增变量自身的值加1&#xff0c;如X--自减变量自身的值减1&#xff0c;如X-- 3.比较运算符 运算符作用>左边是否大于右…...

Mac m1,m2,m3芯片使用nvm安装node14报错

使用nvm安装了node 12/16/18都没有问题&#xff0c;到14就报错了。第一次看到这个报错有点懵&#xff0c;查询资料发现是Mac芯片的问题。 Issue上提供了两个方案&#xff1a; 1、为了在arm64的Mac上安装node 14&#xff0c;需要使用Rosseta&#xff0c;可以通过以下命令安装 …...

LeetCode:63. 不同路径 II

跟着carl学算法&#xff0c;本系列博客仅做个人记录&#xff0c;建议大家都去看carl本人的博客&#xff0c;写的真的很好的&#xff01; 代码随想录 LeetCode&#xff1a;63. 不同路径 II 给定一个 m x n 的整数数组 grid。一个机器人初始位于 左上角&#xff08;即 grid[0][0]…...

安装zsh并美化

0 Zsh 是一种功能强大的 shell&#xff0c;通常用于替代默认的 Bash shell。它为命令行提供了更多的功能&#xff0c;例如自动补全、强大的模式匹配和主题支持等。 Oh My Zsh 是用于管理 Zsh 配置的框架。 powerlevel10k是样式&#xff0c;通过p10k configure脚本可以调节自己…...