深入分析C++中声明与定义的区别

首先谈下声明与定义的区别。
        声明是将一个名称引入程序。定义提供了一个实体在程序中的唯一描述。声明和定义有时是同时存在的。

   如int a;

   extern int b=1;

只有当extern中不存在初始化式是才是声明。其他情况既是定义也是声明。

但是在下列情况下,声明仅仅是声明:

    1:仅仅提供函数原型。如void func(int,int);

    2: extern int a;

    3:class A;

    4:typedef声明

    5:在类中定义的静态数据成员的声明

如:

class A
{
  public:
  static int a;//声明。
};

下列情况下 ,定义仅仅是定义:

   1:在类定义之外,定义并初始化一个静态数据成员。如 A::a=0;

   2:在类外定义非内联成员函数。

声明仅仅是将一个符号引入到一个作用域。而定义提供了一个实体在程序中的唯一描述。在一个给定的定义域中重复声明一个符号是可以的,但是却不能重复定义,否则将会引起编译错误。但是在类中的成员函数和静态数据成员却是例外,虽然在类内它们都是声明,但是也不能有多个。

如:

明白了声明与定义的区别,还需要明白 内部链接、外部链接。只有明白了它们你才会知道开头提出的问题。

在编译时,编译器只检测程序语法和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成目标文件。而在链接程序时,链接器会在所有的目标文件中找寻函数的实现。如果找不到,那到就会报链接错误码(Linker Error)。在VC下,这种错误一般是:Link 2001错误,意思说是说,链接器未能找到函数的实现。

链接把不同编译单元产生的符号联系起来。有两种链接方式:内部链接和外部链接。

如果一个符号名对于它的编译单元来说是局部的,并且在链接时不可能与其他编译单元中的同样的名称相冲突,那个这个符号就是内部链接。内部链接意味着对此符号的访问仅限于当前的编译单元中,对其他编译单元都是不可见的。

static关键字作用在全局变量时,表示静态全局变量。但是作用域仅仅在当前文件作用域内。其他文件中即使使用extern声明也是无法使用的。const也类似。

带有static、const关键字和枚举类型的连接是内部的。

具有内部链接的符号无法作用于当前文件外部,要让其影响程序的其他部分,可以将其放在.h文件中。此时在所有包含此.h文件的源文件都有自己的定义且互不影响。

类的定义具有内部链接,由于它是定义,因此在同一编译单元中不能重复出现。如果需要在其他编译单元使用,类必须被定义在头文件且被其他文件包含。仅仅在其他文件中使用class a;声明是不行的,原因就是类的定义是内部链接,不会在目标文件导出符号。也就不会被其他单元解析它们的未定义符号。理解这一点很重要。

内联函数也具有内部链接。

在一个多文件的程序中,如果一个符号在链接时可以和其他编译单元交互,那么这个名称就有外部链接。外部链接意味着该定义不仅仅局限在单个编译单元中。它可以在.o文件中产生外部符号。可以被其他编译单元访问用来解析它们未定义的符号。因此它们在整个程序中必须是唯一的,否则将会导致重复定义。

非内联成员函数、非内联函数、非静态自由函数都具有外部链接。

内联函数之所有具有内部链接,因为编译器在可能的时候,会将所有 对函数的调用替换为函数体,不将任何符号写入.o文件。

判断一个符号是内部链接还是外部链接的一个很好的方法就是看该符号是否被写入.o文件。

前面说的是定义对链接方式的影响,接下来说下声明对链接方式的影响。

由于声明只对当前编译单元有用,因此声明并不将任何东西写入.o文件。

如extern int a;

int func();

这些声明本身不会影响到.o文件的内容。每一个都只是命名一个外部符号,使当前的编译单元在需要的时候可以访问相应的全局定义。

函数调用会导致一个未定义的符号被写入到.o文件。如果a在该文件中没有被使用,那么没有被写入到.o文件。而func函数有对此函数的调用。也就会将此符号写入目标文件。此后此.o文件与定义此符号的.o文件被连接在一起,前面未定义的符号被解析。

上述声明有可能导致该符号被写入目标文件中。但是以下声明并不会导致该符号写入到目标文件中。

如:

typedef int Int;
Class A;
struct s;
union point; 

 
     它们的链接也是内部的。

类声明和类定义都是内部链接。只是为当前编译单元所用。

静态的类数据成员的定义具有外部链接。如

class A
{
  static int a;//声明。具有内部链接。
};

静态数据成员a仅仅是一个声明,但是它的定义A::a=0;却具有外部链接。

C++对类和枚举类型的处理方式是不一样的。比如:在不定义类时可以声明一个类。但是不能未经定义就声明一个枚举类型。

基于以上的分析,我们可以知道:将具有外部链接的定义放在头文件中几乎都是编程错误。因为如果该头文件中被多个源文件包含,那么就会存在多个定义,链接时就会出错。

在头文件中放置内部链接的定义却是合法的,但不推荐使用的。因为头文件被包含到多个源文件中时,不仅仅会污染全局命名空间,而且会在每个编译单元中有自己的实体存在。大量消耗内存空间,还会影响机器性能。

const和static修饰的全局变量仅仅在当前文件作用域内有效。它们具有内部链接属性。

下面列出一些应该或是不应该写入头文件的定义:

//test.h
#ifndef TEST_H
#define TEST_H
  int a;   //a有外部链接,不能在头文件中定义。
  extern int b=10;//同上。
  const int c=2;//c具有内部链接,可以定在头文件中但应该避免。
  static int d=3;//同上。
  static void func(){} //同上。
  void func2(){} //同a。
  void func3();//可以。仅仅是声明。并不会导致符号名被写入目标文件。
class A
{
  public:
   static int e;//可以,具有内部链接。
   int f;//可以,同上。
   void func4();//声明,内部链接。同上。
};
  A::e=10;//不可以在头文件中包含具有外部链接的定义。符号名别写入目标文件。
  void A:func4()//不可以,类成员函数。外部连接。
{
 //,......
}
#endif
 

相信大家现在明白为什么只在类型声明成员函数,而不实现它是合法的了。也可以回答为什么类的定义可以放在.h文件中。而类的实现可以放在同名的cpp文件中。老师以前的介绍是说编译器会自动寻找同名的cpp文件。其实是因为由于cpp文件中存储的是成员函数的实现,而成员函数具有外部链接特性,会在目标文件产生符号。在此文件中此符号是定义过的。其他调用此成员函数的目标文件也会产生一个未定的符号。两目标文件连接后此符号就被解析。注意static数据成员应该放在cpp文件中。而不能放在.h文件。

有内部链接的定义可以定义在cpp文件中,并不会影响全局的符号空间 。但是在cpp文件作用域中要避免定义(并不禁止)没有声明为静态的数据和函数,因为它们具有外部链接。

 int a;
void func()
{
    ......
} 

上述定义具有外部链接可能会与全局命名空间的其他符号名称存在潜在冲突。如果确实需要使用全局的变量或函数。可以为它们加上static关键字。使其作用域局限在当前文件内,具有内部链接也就不会对全局命名空间产生影响。因为内联函数和静态自由函数、枚举以及const类型的数据都具有内部链接,所以它们可以定义在cpp文件中,而不会影响全局命名空间。

typedef和宏定义不会将符号引入.o文件,它们也可以出现在cpp文件中,不会影响全局命名空间。

typedef 为一个已存在的类型创建一个别名。而不是创建一个新的类型。它不提供类型安全。如

typedef int IntA;
typedef int InB;  
 

在需要IntA的地方使用IntB是不会报错的。它们可以互相替换。因为此我们称它不提供类型安全。但是在定义函数类型时typedef经常使用,可以使定义更清晰。

标准c库提供一个assert宏,用以保证给定的表达式值非零。否则便会输出错误信息并终止程序执行。只有在程序中没有定义NDEBUG时,assert才会工作。一旦定义NDEBUG  ,assert语句将会被忽略 。注意与VC中的ASSERT相区别。ASSERT是vc提供的。当_DEBUG被定义时才会起作用。

在vc的DEBUG模式下_DEBUG会被定义。而在RELEASE模式下NDEBUG会被定义。

好了,相信大家都会明白开头提出的问题了。如果有不明白的,请务必留言哦。如有错误,也请不吝指正!!

以上内容参考自《Large Scale C++ software design》。

(0)

相关推荐

  • C字符串与C++中string的区别详解

    在C++中则把字符串封装成了一种数据类型string,可以直接声明变量并进行赋值等字符串操作.以下是C字符串和C++中string的区别:  C字符串 string对象(C++) 所需的头文件名称  <string>或<string.h> <string>或<string.h> 需要头文件 原因 为了使用字符串函数 为了使用string类 声明 方式 char name[20]; string name; 初始化方式 char name[20]="

  • C与C++ 无参函数的区别解析

    下面是一个C语言的函数和C++的函数验证了这段话 复制代码 代码如下: # include<stdio.h> int fun1(); int main(){ int a = fun1(3, 4); printf("%d\n", a); return 0;} int fun1(int a, int b){ return a + b;} 结果没有任何问题,输出结果为7,也即fun()可以声明fun(int, int) 复制代码 代码如下: # include<iostre

  • C++中memcpy和memmove的区别总结

    变态的命名 我们在写程序时,一般讲究见到变量的命名,就能让别人基本知道该变量的含义.memcpy内存拷贝,没有问题;memmove,内存移动?错,如果这样理解的话,那么这篇文章你就必须要好好看看了,memmove还是内存拷贝.那么既然memcpy和memmove二者都是内存拷贝,那二者究竟有什么区别呢? 先说memcpy 你有没有好好的参加过一场C++笔试.让你写出memcpy的实现,这是多么常见的笔试题啊.现在,拿起你的演算纸和笔;是的,是笔和纸,不是让你在你的IDE上写.写不出来?看下面吧:

  • C/C++动态分配与释放内存的区别详细解析

    1. malloc()函数1.1 malloc的全称是memory allocation,中文叫动态内存分配.原型:extern void *malloc(unsigned int num_bytes); 说明:分配长度为num_bytes字节的内存块.如果分配成功则返回指向被分配内存的指针,分配失败返回空指针NULL.当内存不再使用时,应使用free()函数将内存块释放. 1.2 void *malloc(int size); 说明:malloc 向系统申请分配指定size个字节的内存空间,返

  • C++中继承与组合的区别详细解析

    C++的"继承"特性可以提高程序的可复用性.正因为"继承"太有用.太容易用,才要防止乱用"继承".我们要给"继承"立一些使用规则: 一.如果类A 和类B 毫不相关,不可以为了使B 的功能更多些而让B 继承A 的功能. 不要觉得"不吃白不吃",让一个好端端的健壮青年无缘无故地吃人参补身体. 二.如果类B 有必要使用A 的功能,则要分两种情况考虑: (1)若在逻辑上B 是A 的"一种"(a

  • c++函数中的指针参数与地址参数区别介绍

    比如 一个函数 chat(link &a): chat(ling *a): 前者引入一个地址做形参 是不是可以把一个指针变量p.. 这么用chat(p): 那跟第二个函数 有什么区别呢 都是传地址啊.. 小弟弄不明白~~ chat(int&a); chat(int *a); 这两个函数是完全不同意义的东西,你的理解主要是在int&a和int* a这个类型上面.要注意int&和int*是两个完全不同的类型.int&是引用类型,而int*是指向int类型变量的指针类型.

  • VC中SDK与MFC的区别浅析

    本文浅析了vc中SDK与MFC的区别,对于初学VC的朋友有一定的学习借鉴价值,详情如下: SDK 是指Software Development Kit 软件开发包 MFC 是指Microsoft Foundation Classes 微软函数类库 因此MFC是对API函数的封装,也算是vc里的SDK   用VC编写Windows程序有两种:1. Windwos c方式(SDK),2.C++方式:即对SDK函数进行包装,如VC的MFC,BCB的OWL等. SDK编程就是直接调用Windows的AP

  • C++中指针和引用的区别分析

    从概念上讲.指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变. 而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量). 在C++中,指针和引用经常用于函数的参数传递,然而,指针传递参数和引用传递参数是有本质上的不同的: 指针传递参数本质上是值传递的方式,它所传递的是一个地址值.值传递过程中,被调

  • C与C++中结构体的区别

    C中的结构体和C++中结构体的不同之处:在C中的结构体只能自定义数据类型,结构体中不允许有函数,而C++中的结构体可以加入成员函数. C++中的结构体和类的异同: 一.相同之处:结构体中可以包含函数:也可以定义public.private.protected数据成员:定义了结构体之后,可以用结构体名来创建对象.但C中的结构体不允许有函数:也就是说在C++当中,结构体中可以有成员变量,可以有成员函数,可以从别的类继承,也可以被别的类继承,可以有虚函数. 二.不同之处:结构体定义中默认情况下的成员是

  • c++中虚函数和纯虚函数的作用与区别

    虚函数为了重载和多态的需要,在基类中是有定义的,即便定义是空,所以子类中可以重写也可以不写基类中的此函数! 纯虚函数在基类中是没有定义的,必须在子类中加以实现,很像java中的接口函数! 虚函数 引入原因:为了方便使用多态特性,我们常常需要在基类中定义虚函数. class Cman { public: virtual void Eat(){--}; void Move(); private: }; class CChild : public CMan { public: virtual void

  • 虚函数与纯虚函数(C++与Java虚函数的区别)的深入分析

    c++虚函数1.定义:在某基类中声明为 virtual 并在一个或多个派生类中被重新定 义的成员函数 [1]2.语法:virtual 函数返回类型 函数名(参数表) { 函数体 }3.用途:实现多态性,通过指向派生类的基类指针,访问派生类中同名覆盖成员函数,也就是允许子类override父类同名方法.虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型(也

  • VC中SendMessage和PostMessage的区别

    本文实例讲述了VC中SendMessage和PostMessage的区别.分享给大家供大家参考.具体分析如下: 1.首先是返回值意义的区别,我们先看一下 MSDN 里的声明: 复制代码 代码如下: LRESULT SendMessage (  HWND hWnd,         UINT Msg,         WPARAM wParam,         LPARAM lParam ); 复制代码 代码如下: BOOL PostMessage(  HWND hWnd,         UI

  • c++ 虚函数与纯虚函数的区别(深入分析)

    在面向对象的C++语言中,虚函数(virtual function)是一个非常重要的概念.因为它充分体现 了面向对象思想中的继承和多态性这两大特性,在C++语言里应用极广.比如在微软的MFC类库中,你会发现很多函数都有virtual关键字,也就是说, 它们都是虚函数.难怪有人甚至称虚函数是C++语言的精髓. 那么,什么是虚函数呢,我们先来看看微软的解释: 虚函数是指一个类中你希望重载的成员函数,当你用一个基类指针或引用指向一个继承类对象的时候,你调用一个虚函数,实际调用的是继承类的版本. 这个定

随机推荐