C++|深入理解声明、定义(实现)、初始化及头、源文件的组织-pf文件
原文链接:https://www.shangyouw.cn/wenjian/arc52510.html
程序是对声明和定义的数据的处理。数据可以声明为变量、常量、数据结构或类的数据成员,处理可以声明或实现为函数,或类的成员方法。
我们现代使用的绝大多数计算机都是“存储程序”概念的计算机。也就是说,程序处理的数据和处理数据的代码都保存在可以随机访问的内存中。
数据存储有一个很重要的概念:“要存得进去,取得出来”,且还需要考虑空间(存储空间)和时间(程序运行时间)的效率。
数据存储到内存中,需要考虑几个问题(以下内容中,括号内的内容是程序语言相应的解决方案及使用的关键字):
1 数据需要不需要有保护机制?(常量和变量,常量是对数据的保护,如关键字const,但只有常量不够,正像我们不能只计算3+4、6+7这样的算术运算,我们要能实现有一定通用性的a+b这样的变量运算,所以还需要变量)
2 需要多大空间?(不同的数据类型,如int、unsign int、char、double等,有不同的取值范围,使用不同长度的内存空间)
3 在多大的范围内可见(有效或可以被访问到)?(作用域,如auto和文件域)
我们的程序语言都需要考虑大型程序的需要,例如几万行代码,语言多个文件并组织成一个项目。并且在文件内部还需要通过函数或类的对象组织成模块,既是为了避免代码重复,也是为了提高代码的健壮性(减少了耦合度)、可维护性、可读性。文件除了资源文件以外,一般还区分.h头文件和.cpp实现文件,用.h头文件来实现数据、函数、类的声明,在.cpp文件中来完成其实现或定义,且通过预处理指令#include来实现关连。因为多个文件的存储,所以上述的范围有文件内的块({}之间)范围,有块以外的全局的范围,以及文件之间的范围(在程序语言中称之为文件域)。
4 需要存储多久?(存储期,如static)
5 是编译时分配分配内存空间还是程序运行时分配空间?(如malloc\free、new\delete)
内存对应的地址如果用一个变量存储起来,就是一个指针,这种语法机制可以让程序运行时动态地在内存的堆中申请内存空间来保存和处理数据。
6 怎样命名这个空间(命名规则和命名空间)?
由基本数据类型复合而来的结构数据类型,以及函数都具有与上述相同的语法机制,都是通过标识符(变量名、常量名、数组名、结构体名、对象名、函数名)来寻址。
上述的语法机制的实现,由操作系统给运行的程序分配五块空间来实现,分别是代码区、堆区、栈区、全局和静态变量区、常量区。
考虑了上述的问题之后,还有三个问题需要考虑:
1 合适的分配内存的时间;(声明和定义)
2 合适的首次赋值的时间;(初始化)
3 如何合理地组织多文件?包括头文件.h或.c、.cpp源文件。(声明、定义、实现、初始化并如何合理布局到各文件中)
数据类型是所有程序语言的基础。C++程序的所有功能都是建立在内置于C++语言的基本数据类型基础之上的。数据类型可以告诉数据代表的意义以及程序可以对数据执行的哪些操作,它确定了数据和数据操作在程序中的意义。
为因应大程序的需要,除了基本类型以外,我们还需要结构数据类型,如数组、结构体,或类等自定义类型。为实现模块化的需要,还需要函数这种语法机制。
所以,一个声明通常包含如下几个部分(但是并非都必不可少):存储类型、基本类型、类型限定词和最终声明符(也可能包含初始化列表)。每个声明不仅声明一个新的标识符,同时也表明标识符是数组、指针、函数还是他们的任意组合。
当在VC这样的开发工具上编写完代码,点击编译按钮准备生成exe文件时,VC其实做了两步工作,第一步,将每个.cpp(.c)和相应.h文件编译成obj文件;第二步,将工程中所有的obj文件进行LINK生成最终的.exe文件,那么错误就有可能在两个地方产生,一个是编译时的错误,这个主要是语法错误,另一个是连接错误,主要是重复定义变量等。我们所说的编译单元就是指在编译阶段生成的每个obj文件,一个obj文件就是一个编译单元,也就是说一个cpp(.c)和它相应的.h文件共同组成了一个编译单元,一个工程由很多个编译单元组成,每个obj文件里包含了变量存储的相对地址等。
一、 为什么要区分声明和定义(实现)?
在程序语言中,一般来说,有时并不需要在声明时就马上分配内存空间,而在真正需要的时候才分配内存空间,例如函数的形参、类的声明。另外,函数的声明还可以方便编译器检查函数的一致性。所以,在程序语言中,一般来说:
声明是其类型的声明,用于声明其作用域、文件域、存续期、类型、标识符名称,特别地,与类型在一起的语句就是声明;声明并不直接给成员变量分配内存,这只是告诉编译器,这个声明的类是什么样的?包含哪些数据以及能做什么?编译器根据类声明的成员变量,就知道这个类的对象所需要的内存空间。
而对于自定义类型,例如结构体和类来说,在类型的声明和定义(实现)时都不会分配内存空间,只有在确定好类型之后,用这个类型去定义变量和对象时,才会分配内存空间。
对于函数来说,函数的声明、定义(实现)还只是数据处理的一种方案,真正的实施是在函数调用时,也就是说,只在函数调用时才考虑是否为其形参赋值或为其局部变量分配空间。
声明用于向程序表明变量的类型和名称。所以类型和函数的定义也是一种声明。
对于全局变量,使用extern关键字声明变量名但是不定义它,如:
extern int a; // 声明但是未定义a,在此文件的此处之前,未有int a的定义,定义在其后或其它文件中int a; // 声明和定义同时进行
extern声明不是定义,也不会分配存储空间。它只是说明变量定义在程序的其他地方(声明处的后面或其它文件中)。含有初始化的extern声明被当做是定义,程序中变量可以用extern声明多次,但只能定义一次。(对于静态局部变量而言,其空间限制在语句块中。对于非静态局部变量而言,一是其空间和时间都限制在语句块中,都无需多次声明使用)
在一个程序中,变量有且只有一个定义(是说只分配空间一次,使用时作为右值是取值操作,作为左值是值更新操作)。
对于函数,其作用域是全局的,提供声明的extern可以省略:
extern int f(int a, int b);
而函数的定义只是提供函数体的声明。
把全局声明放在头文件中绝对是个好主意,当需要在多个源文件中共享变量或函数时,需要确保定义和声明的一致性。最好的安排就是在某个相关的.c文件中定义,然后在头.h文件中进行外部声明,以便编译器检查定义和声明的一致性。
C++程序通常由许多文件组成。为了让多个文件访问相同的变量,C++区分了声明和定义。定义和声明有相同的时候,但声明的主要目的是表明变量的类型和名称,而定义的主要目的是为变量分配存储空间。
默认的声明:块中变量默认为auto(auto自动变量是较新的C++版本新增的数据类型,声明变量时,用auto代替变量类型,由编译器根据初始字面值来判断变量类型。),函数默认为extern的文件域。
对于类来说,成员声明还包括声明其访问的权限,如private、public、protected(在继承关系中),friend(对访问权限的突破)等。
二、用头文件和源文件实现声明和定义的分工
前面是考虑类型声明的问题,这里是考虑在头文件在进行声明,在源文件中实现声明的分工问题。
理解多处声明,一处定义(内存空间分配一次,作为右值的值引用或值更新可以多次,函数的实现只能有一次,原型声明可以是多次):
局部变量:声明和定义在调用时(main函数调用,或main函数调用其他函数时)同时进行(同时分配内存);
全局变量:一处定义,多次(多处)extern声明(声明并不分配内存,定义时分配内存);
函数(涉及到形参、实参)的声明、定义、调用(调用时分配内存);
结构体(涉及到成员属性)的声明、实例化(实例化时分配内存);
类(涉及到成员变量和成员函数)的声明、成员函数的定义、实例化(实例化时分配内存);
头文件.h提供声明信息,包括函数、结构体、类等的声明以及宏定义和全局变量的extern声明。源文件.c或.cpp提供函数和类方法实现信息以及全局变量的定义(声明类型和初始化值),这样的程序的逻辑结构更清晰,源文件包含头文件的引用以及相关函数或类方法的实现。
extern int i; //声明外部整形变量,一般建议放到头文件中(.h文件)
char * f(); //声明函数,一般建议放到头文件中(.h文件)
变通但不推荐的做法是:如果想使用另一个文件中的全局变量定义int i,你可以不包含头文件,而直接写extern int i;。推荐的做法是在头文件中写extern int i;,而在需要的源文件中去包含这个头文件。
调用库时也时要用头文件去包含其声明,由其声明链接其具体实现。
下面的语句属于条件编译语句,意思是如果没有 define FUN_H 就 define FUN_H ,如果之前 define 过,#ifndef 到 #endif 的代码段就不参与编译了,这样可以避免 #ifndef 到 #endif 的代码段被重复包含。FUN_H 当然也可以取其他名字,只需要确保唯一性就可以了。
#ifndef _FUN_H_ #define _FUN_H_ //... #endif
为什么不直接包含 .c 文件呢? 在 main.c 文件里直接 #include“fun.c”不更方便吗?当然,这样编译也能通过,可是以后要是又有一个模块需要用到 fun.c 中定义的函数呢?再包含一次 fun.c ?这样不就相当于一个函数有多处定义了吗?这样在程序链接阶段就会有麻烦,或者根本无法生成可执行程序。如果包含的是头文件,那无论包含多少次(声明了多次),函数也只有一处定义,链接是不会有问题的了。(用头文件多次声明(include包含),一次定义(在源文件中))
作为一般规则,尽量不要把实际的代码(如函数体)或全局变量定义(即定义和初始化实例)放入头文件中,而应该把下面所列的内容放入头文件.h中:
1 宏定义(预处理#define);
2 结构、联合、枚举、类的声明;
3 typedef声明;
4 外部函数声明(隐式地有包含关键字extern);
5 全局变量声明(显式地有包含关键字extern);
(只在头文件中声明外部链接的元素)
需要全局或自定义类型声明的源文件用预处理命令#include包含该头文件,相当于把其内容复制到了文件的头部,形成全局声明或自定义类型的声明,这样的头文件能够被多个源文件包含,但函数、类方法的具体实现却在源文件中,这样形成的格局就是“多次声明,一次定义(实现)”。
为什么要分为很多头文件?如果所有的库写到一个库文件中,你写的程序会很大,所以头文件只是把一些相似功能的函数写到一个头文件中,这样引用时则数据比较少。
三、 初始化就是给给变量或常量首次赋值
通常仅声明的变量是没有初始值的(有一个历史值或垃圾值的随机值,当然也有可能是0值或NULL值),给变量填入数据最直接的办法就是使用赋值运算符将值赋予变量。赋值运算符将值与变量绑定起来,也就是说,值写入变量所引用的内存单元。在给变量、常量、指针、数组赋真正需要的实际值之前,为了安全的考虑,需要先给其赋一个0值或NULL值,或一个确定的值。
变量的声明当然也可包含对变量的初始化,但是不赋显式的初始值的时候,某种特定的缺省初始化也会进行。
除了直接赋值初始化以外,也可以使用初始化函数将某一块内存中的内容全部设置为指定的值(menset()函数通常为新申请的内存做初始化工作):
void *memset(void *s, int ch, size_t n); //结构体和数组可以使用={0}来初始化零值
编译器会自动给全局变量和静态变量初始化一个0值或NULL。
C\C++的编译对于局部变量并未自动初始化零值,可以是出于效率的考虑,因为赋值也是需要运行时间的,特别是大批量赋值时,因为早期的计算机的速度相对来说是较慢的。
编译器强制要求常量在的声明、定义、初始化同时进行。(因为常量只有一次赋值或更新值的机会。)
对于函数中包含的数据来说,函数调用做了两件事情:用对应的实参初始化函数的形参(创建变量并赋值),并将控制权转移给被调用函数。主调函数的执行被挂起,被调函数开始执行。函数的运行以形参的(隐式)定义和初始化开始。
函数被调用时,系统为每个形参分配内存单元,也就是相当于一个局部变量的声明后的初始化。
另外,函数调用只能出现在自动变量(即局部非静态变量)的初始式中。
函数指针的初始化:
extern int func(); int (*fp)() = func; // 当一个函数名出现在这样的表达式中,它就会“退化”为一个指针 // 即隐式地取出了地址值,有点类似数组名的行为
C++的引用必须指向一个对象,C++要求引用必须初始化,并且没有NULL引用这种概念。
可以像其他数组那样声明并初始化字符串:
char carr[] = {'w','w','u','h','n','\0'};
也可以用字面量初始化字符串的简捷方式:
char carr[] = "wwuhn";
当然也可以使用指针的方式:
char *cp = "wwuhn";
结构体也支持声明时定义并同时初始化的集合赋值操作,声明以后不再支持集合赋值的操作。
定义二维数组时,若按一维格式初始化,则第一维的长度可以省略,此时,系统可根据初始化列表中值的个数及第二维的长度计算出省略的第一维长度,但无论如何,第二维的长度不能省略。没有初始化时,第一维和第二维的长度都不能省略。
类的静态成员必须在类中声明,在类外初始化(或定义)。
对于类来说,一般用构造函数去初始化对象。
在类中,纯虚函数使用纯指示符(=0)声明,代码如下:
virtual CalArea() = 0;
类的构造函数的形参指定了创建类类型对象时使用的初始化式。通常,这些初始化式会用于初始化新创建对象的数据成员。构造函数通常应确保其每个数据成员都完成了初始化。
四、作用域、存储期、文件域
复合语句,通常被称为块,是用一对花括号括起来的语句序列(也可能是空的)。块标识了一个作用域,在块中引入的名字只能在该块内部或嵌套在块中的子块里访问。通常,一个名字只从其定义处到该块的结尾这段范围内可见。
对于在控制语句中定义的变量,限制其作用域的一个好处是,这些变量名可以重复使用而不必担心它们的当前值在每一次使用时是否正确。对于作用域外的变量,是不可能用到其在作用域内的残留值的。
而全局作用域可以用来共享数据,但往往也就有了安全的隐患。
作用域也可以理解为一种上下文。
在C++中,每个变量名都与唯一的实体(例如变量,函数和类型等)相关联。尽管有这样的要求,还是可以在程序中多次使用同一个变量名,只要它用在不同的区域中,且通过这些区域可以区分该变量名的不同意义。用来区分变量名的不同意义的区域称为作用域。大多数作用域是用花括号来划定界限的。
存储类型是从变量的存在时间(即生存期)来划分变量。变量的存储类型可分为静态存储方式和动态存储方式。对于动态存储变量,当程序运行到该变量处时才为其分配存储空间,当程序运行到该变量所在作用域的结束处时自动收回为其分配的存储空间,因此它的生存期为所在作用域。在程序开始执行时就为其分配存储空间,直到程序结束时,才收回变量的存储空间,这种变量称为静态存储空间,其生命周期为整个程序执行的过程。
在C++中,变量的存储类型有自动类型、寄存器类型、静态类型、外部类型等4种。
1 自动类型变量(auto)
自动类型只能是局部类型的变量,属于动态存储类型。
2 静态类型变量(static)
static,即在程序运行的过程中静态变量始终是占用一个存储空间。静态变量只能在他的作用范围内使用,使用局部静态变量是为了在下次调用该函数时,能使用上次调用后得到的该变量的值。
3 寄存器类型变量(register)
属于动态存储类型,编译器不为寄存器类型的变量分配内存空间,而是直接使用CPU的寄存器。以便提高对这类变量的存取速度。主要用于控制循环次数等不需要长期保存值得变量。
4 外部类型变量(extern)
外部类型变量必须是全局变量,在C++中,有两种情况需要使用外部类型变量。一种是在同一源程序文件中,当在全局的定义之前使用该变量时,在使用前要对该变量进行外部类型变量声明。另一种是当程序有多个文件组成时,若在一个源文件中要引用在另一个源文件中定义的全局变量,则在引用前必须对所引用的变量进行外部声明。
如果在某文件中定义的全局变量不想被其他文件所调用,则必须用关键字static将该变量声明为静态全局变量,也就是说,静态全局变量只能供所在的文件使用。此时关键字static不再是存储类型,而是文件域的定义了,编译器会在上下文中去理解。
五、数据类型
数据类型是程序语言的基本要素。为什么需要数据类型?分类是人类认识外界事物的很重要的手段,如植物学、动物学的门纲目科属种的分类就是这两个学科很重要的概念。不同类别的事物具有不同的特征,具有不同的属性和行为方式。
在C/C++中,数据类型分为两种,简单类型和结构类型。简单类型包括有整数类型、字符类型、浮点类型、指针类型、枚举类型和void类型等。结构类型包括有数组、字符串、记录和文件等。C/C++的基本数据类型属于简单类型。用户可以创建的所有数据类型都是根据基本类型定义的。
根据基本数据类型声明的变量可以告诉编译器以下信息:
1 需要的内存空间;
2 取值的范围;
3 可以执行的操作(如可以使用什么运算符,使用运算符的规则及达到的效果);
找到正确的数据表示不仅仅是选择一种数据类型,还要考虑必须进行哪些操作。也就是说,必须确定如何储存数据,并且为数据类型定义有效的操作。例如,C实现通常把int类型和指针类型都储存为整数,但是这两种类型的有效操作不相同。例如,两个整数可以相乘,但是两个指针不相同;可以用*运算符解引用指针,但是对整数这样做豪无意义。C语言为它的基本类型都定义了有效的操作。但是,当你要设计数据表示的方案时,你可能需要自己定义有效操作。在C语言中,可以把所需的操作设计成C函数来表示。简而言之,设计一种数据类型包括设计如何储存该数据类型和设计一系管理该数据的函数。还要注意的是,C并未很好地实现整数。例如,整数是无穷大的数,但是2字节的int类型只能表示65536个整数。因此,不要混淆抽象概念和具体的实现。
为了因应复杂问题和大程序的需要,只有基本类型是不够的,需要通过复合去组合基本类型去形成复合(结构)数据的自定义类型。例如一年的12个月的天数,一辆车的品牌、重量、速度,同时,一辆车不只是有属性,还有它的行为方式,如开动、行驶、停车、倒车等。程序语言就是对世界的模拟与表示,这些概念形成程序语言的语法就是数组、结构体、类、及其相互复合。
复合(结构)数据不像单个的基本数据类型(原子元素),因为其是多个元素组成的,用一个统一的标识符来表示这个集合体,自然就需要考虑其元素的相互关系,例如你求出10个城市之间的最短路径,自然就要考虑这10个城市的交通网络。
一个数据集合中元素的关系称为逻辑关系,逻辑关系有一对一的线性关系、二对多的树型关系,多对多的图形关系,还有只属于于同一个集合,除此以外无其他关系的集合关系。
除了逻辑关系以外,还要考虑如何存储?我们知道,内存是可以随机访问的,是因为其内存单元都有一个地址对应,内存单元之间是线性的逻辑关系。如果按一个整体分配到一个连续的内存区域中,称为顺序存储,这种顺序存储可以映射元素的线性逻辑关系。只要知道了第一个元素的位置,就可以顺藤摸瓜,找到其它元素。但顺序存储有其优势,也有其弊端,大块的数据往往有可能存储失败。同时元素的增、删会影响到其它元素的位置,对于需要大量这种操作的结构类型不利。解决的方法就是另外一个储存方式,对每一个元素除了其元素本身的数据以外,再增加一个数据域,用来表示其相邻元素的地址,这样,与顺序存储相同,只要知道了第一个元素的位置,也可以按其存储的地址域,可以依次访问到各自的相邻元素,这就是链式存储。这时元素称为一个节点,可以用一个结构体来表示,这个节点体中有两个数据域,一个数据域代表元素本身,一个就是地址域。
对于树型关系和图形关系,与链式存储的思路一样,逻辑关系也可通过增加数据域去表示,如用一个邻接表去表示一种图形关系(树型是图形的特例)。当然,简单直接的方法就是用一个二维表去表示。
如同基本数据类型要考虑其值域及可以使用的操作(操作符)一样,复合(结构)数据类型也要考虑其数据结构,和可以定义的操作(如元素的增、查、删、改、遍历、排序、查找等),这就是数据结构的概念。排序、查找这些操作可以考虑不同的方法,如分治法、穷举法等,在程序中就是算法的概念。特定的操作可以形成函数,可以固定下来,形成函数库或类库。
复合(结构)数据不仅是表示复杂问题的需要,同时也提高了编程的颗粒度,就如同生产一个产品,不能都是从零开始生产每一个部件,而是可能组装。
数据类型是一组性质相同的值集合以及定义在这个值集合上的一组操作的总称。数据类型定义了两个集合,即该类型的取值范围以及该类型中可允许使用的一组运算。例如,高级语言中的数据类型就是已经实现的数据结构的实例。从这个意义上讲,数据类型是高级语言中允许的变量种类,是程序设计语言中已经实现的数据结构(即程序中允许出现的数据形式)。
ADT(Abstract Data Type)定义了一个数据对象、数据对象中各元素间的结构关系以及一组处理数据的操作。ADT通常是指由用户定义且用以表示应用问题的数据模型,由基本的数据类型组成,并包括一组相关服务操作。
ADT{ 数据对象:<数据对象的定义> 结构关系:<结构关系的定义> 基本操作:<基本操作的定义> } ADT
如图这种数据结构,就包括其元素、元素关系及关于图的操作。
#define宏定义只是一个简单的替换,编译器并不会做类型检查。
静态语言:与动态语言相比较,在写程序时,所有变量必须声明其数据类型。例如Java就是静态语言,其声明变量时,必须给定数据类型,并初始化。强类型定义语言:强制数据类型定义的语言。也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。强类型定义语言带来的严谨性能够有效的避免许多错误弱类型定义语言:数据类型可以被忽略的语言。它与强类型定义语言相反, 一个变量可以赋不同数据类型的值。
六、 变量和常量
常量是不可以改变值的量,变量是可以改变值的量,常量在定义时必须初始化,变量可以在定义时不初始化。常量不可以寻址,它的地址不允许赋给非常量指针,变量可以寻址。常量有相对较高的编译执行效率。
变量,顾名思义就是在程序运行中,可以被改变的量。变量在定义后为程序提供了一个有名字的内存区域,编程者可以通过程序对它进行读写和处理。变量值的改变是通过赋值操作进行的。变量的基本使用示例代码如下:
double pi = 3.14; //定义double型变量pi pi = 3.1415; pi = 3.1415926; //可以通过赋值不断改变pi的值 scanf( "%lf", &pi );
常量是对数据的一种保护方式,也是一种统一字面量值的一种方式。
常量类型:
const声明的常量;
constxpr声明的常量表达式;
enum声明的枚举常量;
#define定义的常量(已摒弃,不推荐);
字面常量;
对于下面的赋值操作,虽然可以正常编译,但是赋值语句却并不起作用,因为“test”是常量,是不能再被赋值的,编译器会自动把它定义为常量指针。
char *p = “test”; *p = ‘p’;
七、 复合声明
基本数据类型、结构数据类型、指针、函数、数组可以相互组合,所以,程序中的声明、定义、初始化除了基本数据类型的声明、定义、初始化以外,还有上述概念的相互组合的复合声明、定义和初始化。
下面到底哪个是数组指针,哪个是指针数组呢:
int *p1[10]; int (*p2)[10];
“[]”的优先级比“*”要高。p1 先与“[]”结合,构成一个数组的定义,数组名为p1,int *修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含10 个指向int 类型数据的指针,即指针数组。至于p2 就更好理解了,在这里“()”的优先级比“[]”高,“*”号和p2 构成一个指针的定义,指针变量名为p2,int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚p2 是一个指针,它指向一个包含10 个int 类型数据的数组,即数组指针。
在复合声明中,相对于数据运算符和函数运算符,指针声明运算符*的优先级是最低的。
同时,()既是函数运算符(通常位于复合声明的最后),()也是声明最高优先级的运算符。
如果没有用()来改变优先级,你就可以按从左至右的顺序去理解,核心(最终类型)落在最后(前面的指针符号相当于是修饰):
int *ap[] 指针数组,元素是指针的数组;
int *fp() 指针函数,返回指针的函数;
如果有用()来改变优先级,,核心(最终类型)落在最后的声明最高优先级()内的内容(其它符号相当于是修饰):
int (*ap)[] 数组指针,指向一个数组的指针;
int (*fp)() 函数指针,指向一个函数的指针;
例如,下面声明可以按上面的规则去理解:
char * (*pfpc)();
把前面括号的内容放到最后去理解,剩下的从左至右:
指针函数指针,是一个指针,一个指向指针函数的指针,指针指向的函数返回一个指针。
注意:运算符*在C语句中是有重载的,在不同的上下文中,有不同的功能。当左右是数字时,是乘法运算符,当与int、float、char等数据类型在一起声明复合数据类型时,它是用于指针声明的,当在上述两种情况以外时,则是做为指针解引用而存在的。同样的,()也有不同的上下文。
typedef为C语言的关键字,typedef主要是为复杂的声明定义简单的别名。。这里的数据类型包括内部数据类型(int,char等)和自定义的数据类型(struct等)。
八、static和extern
static在C++中有4个作用,
1 声明局部变量为静态的存续期
函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值。
2 声明全局变量为静态的文件域
声明了static的局部变量的值可以持续到程序结束,是时间的概念;
声明了static的全部变量可以访问的空间限制在文件以内,是空间的概念。当然不影响其时间的存续期。与其相对的是关键字extern,表示在要使用此位置前未有定义,要使用别外定义的全局变量,所以,这里的关键字static可以理解为internal。
在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其他函数访问。
3 声明函数为静态的文件域
在模块内的static函数只可以被这一模块内的其他函数调用。这个函数的使用范围被限制在声明它的模块内。
4 在类中声明静态成员
4.1 静态成员变量
1)在编译阶段就分配空间,对象还没有创建。
2)必须在类中声明,在类外初始化(或定义)。
3)归同一个类的所有对象所有,共享同一个静态变量。在为对象分配的空间中不包含静态成员所占空间
4)有权限限制:private静态变量和 public静态变量。
在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝。
4.2 静态成员函数
在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
一般可以考虑将需要有文件作用域的变量集中在一个头文件中用extern去声明,集中在一个cpp文件中去初始化定义,在需要这些文件域的变量的cpp文件中去引用这个头文件。而static声明和定义的全局变量,都把它放在原文件中而不是头文件(因为你此时声明static的目的就是为了不跨文本使用)。
extern当它与"C"一起连用时,如: extern "C" void fun(int a, int b); 则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的, C++的规则在翻译这个函数名时会把fun这个名字变得面目全非,可能是fun@aBc_int_int#%$也可能是别的,这要看编译器的"脾气"了(不同的编译器采用的方法不一样),为什么这么做呢,因为C++支持函数的重载啊。
九、const
1 欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了。
2 对指针来说,可以指定指针本身为const;也可以指定指针所指的数据为const(不能用指针去修改其指向的内存单元的值),或二者同时指定为const。
const char *p; //指针指向一个不能用指针修改的值,可用其它方式去修改 char const *p; //指针本身是一个常量,不能修改为指向其它内存单元 char *const p; //同上
3 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值。
4 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量。
5 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
当const单独使用时它就与static相同,而当与extern一起合作的时候,它的特性就跟extern的一样了!
十、程序员通常需要处理下述5个内存区域:
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。下面分别来介绍:
栈区(stack), 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆区(heap),一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。C语言一般使用malloc()函数和realloc()函数申请,而C++一般使用new运算符申请堆空间;
全局区(静态区)(static),全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
文字常量区,常量字符串就是放在这里的,程序结束后由系统释放 。
程序代码区,存放函数体的二进制代码。
除了内存空间以外,也可以将数据申请存储到寄存器,以获得更快的处理速度。
局部变量的局限性是不会持久化,函数返回时,局部变量将丢弃。全局变量解决了这个问题,但代价是整个程序中都能访问它,这导致代码容易出现bug,难以理解和维护。将数据放在堆中可解决这两个问题。可将堆视为大块内存,其中有数以千计的文件架等待您存储数据,但您不能像栈那样标识这些文件架。您必须询问预留的文件架的地址,将其存储到指针,然后丢掉。每当函数返回时,都会清理栈。此时,所有局部变量都不在作用域内,从而从栈中删除。只有到程序结束后才会清理堆,因此使用完预留的内存后,您需要负责将其释放。让不再需要的信息留在堆中称为内存泄露。堆的优点在于,在显式释放前,您预留的内存始终可用。如果在函数中预留堆听内存,在函数返回后,该内存仍然可用。以指针访问堆中内存(而不是全局变量)的优点是,只有有权访问指针的函数才能访问它指向的数据。这提供了控制严密的数据接口,消除了函数意外修改数据的问题。如果不能从堆中分配内存(因为内存资源有限),将引发异常。异常是处理错误的对象。
十一、综合与补充
C语言有4种作用域(标识符声明的有效区域):函数、文件、块、原型(函数原型声明的形参)。
C语言有4种命名空间:
1 行标label,是goto的目的地;
2 标签tag,结构、联合和枚举的名称;
3 结构或联合的成员;
4 普通标识符,函数、变量、类型定义名称和枚举常量;(C++有统一的命名空间std,当用#include命令包含库时,需要声明其命令空间,不然其引用的对象前面都要加上std::来宣称其命名空间)
C语言的3种“连接类型”:
1 外部连接:全局、非静态变量和函数,在所有的源文件中有效;
2 内部连接:文件作用域内的静态函数和静态变量;
3 无连接:局部变量及类型定义(typedef)名称和枚举常量;
所有变量的作用域都开始于变量的声明处,换句话说,变量必须先声明再使用。
声明指针为什么需要声明类型,原因之一就是当指针运算产生偏转或读取数据时,它知道按不同的类型需要读取多少内存或偏转多少位置?
函数包括函数声明、函数定义、函数定义的话,如果需要修改函数时,最好不需要三个方面都要修改,只需要修改一个地方而保持另外两部分的稳定是最好的。如果接口设计良好的话,则只需要更改函数定义即可。
函数的原型或声明,是为了在函数调用时方便编译器的检查。
函数模板是对函数中参数和返回值的一种泛化。
声明一个函数模板的格式是:
template <<模板形参表声明>><函数声明>
类模板就是一系列相关类的模板或样板,这些类的成员组成相同,成员函数的源代码形式相同,所不同的只是针对的类型(数据成员的类型以及成员函数的参数和返回值的类型)。对应类模板,数据类型本身成了参数,因而是一种参数化类型的类,是类的生成器。类模板中声明的类称为模板类。
声明一个模板类的格式是:
template <<模板形参表声明>><类声明>
声明变量并不是多此一举,因为当你在某处使用这个变量的某一个字母写错时,会编译报错?如果没有变量声明机制,这样的错误发现不了,可能会出现意料之外的错误。
malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。使用malloc函数需要指定内存分配的字节数并且不能初始化对象,New会自动调用对象的构造函数。delete会调用对象的destructor,而free不会调用对象的destructor。
C语言标准库中有一个语言初始化函数memset()。作用是将某一块内存中的内容全部设置为指定的值, 这个函数通常为新申请的内存做初始化工作。
for循环特别适用于需要初始化和更新的循环。使用逗号运算符可以在for循环中初始化和更新多个变量。
数组可以初始化,但不能整体赋值,如
char a[] = "hello";
但不能:
char a[11]; a = "hello";
可以这样操作:
strcpy(a,"hello");
函数可以看作是由程序员来定义的操作,是划分程序的各个程序块,与内置操作符相同的是,每个函数都会实现一系列的计算,然后(大多数时候)生成一个计算结果。但与操作符不同的是,函数有自己的函数名,而且操作数没有数量限制。与操作符一样,函数可以重载,这意味着同样的函数名可以对应多个不同的函数。
在类的定义外面定义成员函数必须指明它们是类的成员:
double Sales_item::avg_price() const
计算机科学领域已开发了一种定义新类型的好方法,用4个步骤完成从抽象到具体的过程。
1 建立抽象:提供类型属性和相关操作的抽象描述。
2 建立接口:开发一个实现ADT的编程接口。也就是说,指明如何储存数据和执行所需操作的函数。例如在C中,可以提供结构定义和操控该结构的函数原型。这些作用于用户定义类型的函数相当于作用于C基本类型的内置运算符。需要使用该新类型的程序员可以使用这个接口进行编程。
3 实现接口:编写代码实现接口。这一步至关重要,但是使用该新类型的程序员无需了解具体的实现细节。
4 使用接口。
上面的抽象可以是函数、结构体或类的抽象,上面的接口可以是函数原型,也可以是类的方法声明