在 C/C++ 里,"声明"和"定义"是两个经常被混用、但语义完全不同的概念。很多编译错误(比如 multiple definition of ...、undefined reference to ...)追根溯源,都是因为没分清这两者。本文从概念、语法、内存、链接几个角度把这件事讲透,并给出工程中的正确写法。
一、一句话概括
声明 是告诉编译器"有这么个名字,它是什么类型";定义是为这个名字分配存储空间(或给出函数体)。
定义一定也是声明,但声明不一定是定义。
换句话说:声明是"承诺",定义是"兑现"。一个变量可以被声明很多次,但只能被定义一次(这就是 ODR,单一定义规则)。
二、变量的声明与定义
2.1 语法上的区分
对于变量,最直观的区分方式是看有没有初始化器 ,以及是否使用了 extern 关键字。
| 写法 | 性质 | 说明 |
|---|---|---|
int a; |
定义 | 分配 4 字节(通常),值未初始化 |
int a = 10; |
定义 | 分配空间并初始化 |
extern int a; |
声明 | 不分配空间,表示"别处有定义" |
extern int a = 10; |
定义 | 带初始化器的 extern 仍然是定义 |
static int a; |
定义 | 内部链接,仅本翻译单元可见 |
关键点:extern 加上初始化器就退化为定义。这是很多人踩过的坑。
2.2 从内存角度看
定义会在目标文件中产生一个符号,并占据一段存储:
- 全局/静态变量:位于
.data(已初始化)或.bss(未初始化/零初始化)段; - 局部变量:位于栈上(或寄存器中);
- 动态分配:位于堆上。
而纯声明不会产生任何存储,它只在符号表里留下一个"未定义符号(UND)",等待链接器去别的地方找。
2.3 从链接角度看(最容易出错的地方)
假设有两个源文件:
cpp
// a.cpp
int g_value = 42; // 定义,产生强符号
// b.cpp
int g_value = 100; // 又一个定义,产生强符号
链接时会直接报错:
text
multiple definition of `g_value'
正确做法是:在头文件中用 extern 声明,在且仅在一个源文件中定义。
cpp
// config.h
#pragma once
extern int g_value; // 声明,可被多个 .cpp 包含
// config.cpp
#include "config.h"
int g_value = 42; // 定义,只有这一处
// main.cpp
#include "config.h"
#include <iostream>
int main() {
std::cout << g_value << std::endl; // 使用
return 0;
}
2.4 C 与 C++ 的一个差异
在 C 语言中,int a; 在文件作用域下是暂定定义(tentative definition) ,同一个翻译单元里出现多次 int a; 是允许的,最终合并为一个定义。而 C++ 没有暂定定义的概念,同一作用域内重复定义会直接报错。
另外,C99 之前不允许在 for 循环里声明变量,C++ 一直允许,这也是两者在"声明位置"上的常见差异。
三、函数的声明与定义
函数的区分更简单:带函数体的是定义,不带函数体(只有分号)的是声明。
cpp
// 声明(函数原型)
int add(int a, int b);
// 定义
int add(int a, int b) {
return a + b;
}
函数声明可以出现任意多次(只要签名一致),函数定义在整个程序中只能有一次(除非是 inline 函数或模板,它们允许在多个翻译单元中出现相同定义)。
3.1 inline 与模板的特殊性
inline 函数和模板函数/类允许在头文件中定义,并被多个源文件包含。链接器会把它们合并成一份,这并不违反 ODR。这也是为什么标准库的模板实现都写在头文件里。
四、类与结构体
对于类类型,情况稍微复杂一些:
- 类定义 :
class Foo { ... };这个整体叫"类定义",它同时完成了类型的声明; - 前向声明 :
class Foo;只声明"有这么个类型",不能用于创建对象,但可以用于指针/引用; - 成员函数 :类内直接写函数体是定义;只写声明、类外写
Foo::bar() { ... }也是定义; - 静态成员变量 :类内
static int count;是声明,必须在类外定义int Foo::count = 0;(C++17 起可用inline static在类内定义)。
cpp
class Foo {
public:
void bar(); // 成员函数声明
static int count; // 静态成员声明
inline static int total = 0; // C++17:类内定义
};
void Foo::bar() { } // 成员函数定义
int Foo::count = 0; // 静态成员定义
五、常见误区与排查
5.1 头文件里直接定义变量
这是最典型的错误。头文件被多个源文件包含,就会产生多个定义:
cpp
// bad.h
int g_counter = 0; // 每个包含它的 .cpp 都定义一次 → 重复定义
正确写法有三种:
- 头文件用
extern声明,源文件定义; - C++17 起使用
inline变量:inline int g_counter = 0;; - 使用
static(每个翻译单元一份副本,注意语义不同)。
5.2 只声明不定义
如果只写了 extern int g_value; 却没有任何地方定义它,编译能过,链接会报:
text
undefined reference to `g_value'
排查思路:确认定义所在的源文件是否参与了编译和链接,以及是否被条件编译宏排除。
5.3 声明与定义类型不一致
cpp
// a.cpp
int g_value = 42;
// b.cpp
extern double g_value; // 类型不匹配,行为未定义
这种错误编译器往往不会报,但运行时会读到错误的数据。务必让声明和定义的类型、const 修饰完全一致。
5.4 const 变量的链接属性
在 C++ 中,命名空间作用域的 const 变量默认具有内部链接 ,相当于隐式 static。所以头文件里写 const int kMax = 100; 不会导致重复定义,但每个翻译单元各有一份副本。若希望全局唯一,应写 extern const int kMax; 并在源文件中定义。
六、快速判断口诀
- 看到
extern且没有初始化器 → 声明; - 看到初始化器 → 定义;
- 函数带
{ }→ 定义,只有;→ 声明; - 类内静态成员只有类型和名字 → 声明,需要在类外定义;
- 头文件里放声明,源文件里放定义,是工程上的黄金法则。
七、小结
声明解决的是"编译器怎么认识这个名字",定义解决的是"这个名字对应哪块内存或哪段代码"。理解这一点,就能解释绝大多数与符号相关的编译、链接错误。实际工程中遵循"头文件声明、源文件定义"的组织方式,配合 extern、inline 变量和 C++17 的新特性,可以既保证可读性又避免重复定义问题。