c++ QT学习笔记

#有关头文件的作用

所谓 #include 的意思呢,就是把另一个文件里的东西复制到这里来,无论你的文件后缀是 h 还是 cpp 都是一样的
楼主的错误是怎么产生的呢?你可以试试写 a.cpp  b.cpp 两个文件,两个文件中有一模一样的两个函数,是不是产生了和你的问题中同样的错误?因为你有两个一模一样的函数,编译器不知道怎么区分它们了。
现在把 a.cpp 中的内容全部去掉,换成 #include "b.cpp"VC在编译的时候,首先将 #include "b.cpp" 替换成了 b.cpp 的内容,于是就和刚才的情形一致了。这时再进行编译,当然就会出一样的错误了。
那么 .h 文件为什么没有类似的问题呢?一是 .h 文件并没有定义,都是一些声明,声明可以重复多次(你只是在告诉编译器,工程中有这么个东西,具体在哪,请到别处找找),而定义不行。二是 VC 并不会编译 .h 文件,如果你把项目中的 .h 文件全部移除(不是删除!),程序照样可以编译。

回到楼主的问题,如果你要调用另一个 .cpp 文件中的函数,只要在使用它的文件中声明一下就可以了。比如:"int Foo( int a, int b );"(注意没有函数体这个声明告诉编译器你要一个名叫 Foo, 接受两个整型参数,返回整型的函数,编译器自动会帮你去找。只要项目中任一个 .cpp 文件定义了这样一个函数,编译器就可以找到。

现在,假设你有很多个函数,并且有很多个 .cpp 文件要用到它们,那么,岂不是需要在每个文件中写上一大堆的声明?这便是 #include 的用武之地了。在一个 .h 文件写上所有的声明,然后在每个 .cpp 文件中 #include 它,于是编译器就会把 #include "xx.h" 替换成 xx.h 文件中的实际内容,于是你的每个 .cpp 文件中都有这些声明了,都可以自由地使用这些函数了。

 

#mutalbe的中文意思是可变的,易变的,跟constant(既C++中的const)是反义词
  在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。

  我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰。
  下面是一个小例子: 

class ClxTest
{
 public:
  void Output() const;
};

void ClxTest::Output() const
{
 cout << "Output for test!" << endl;
}

void OutputTest(const ClxTest& lx)
{
 lx.Output();
}


  类ClxTest的成员函数Output是用来输出的,不会修改类的状态,所以被声明为const的。
  函数OutputTest也是用来输出的,里面调用了对象lxOutput输出方法,为了防止在函数中调用其他成员函数修改任何成员变量,所以参数也被const修饰。
  如果现在,我们要增添一个功能:计算每个对象的输出次数。如果用来计数的变量是普通的变量的话,那么在const成员函数Output里面是不能修改该变量的值的;而该变量跟对象的状态无关,所以应该为了修改该变量而去掉Outputconst属性。这个时候,就该我们的mutable出场了——只要用mutalbe来修饰这个变量,所有问题就迎刃而解了。
  下面是修改过的代码:

class ClxTest
{
 public:
  ClxTest();
  ~ClxTest();

  void Output() const;
  int GetOutputTimes() const;

 private:
  mutable int m_iTimes;
};

ClxTest::ClxTest()
{
 m_iTimes = 0;
}

ClxTest::~ClxTest()
{}

void ClxTest::Output() const
{
 cout << "Output for test!" << endl;
 m_iTimes++;
}

int ClxTest::GetOutputTimes() const
{
 return m_iTimes;
}

void OutputTest(const ClxTest& lx)
{
 cout << lx.GetOutputTimes() << endl;
 lx.Output();
 cout << lx.GetOutputTimes() << endl;
}

  计数器m_iTimesmutable修饰,那么它就可以突破const的限制,在被const修饰的函数里面也能被修改。

函数重载与虚函数的区别

重载函数,重载可以看作是静态的多态。函数重载的返回类型及所带的参数必须至少有一样不完全相同,只需函数名相同即可。

基类中定义的虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型及参数的先后顺序,都必须与基类中的原型完全相同。

重载虚函数时,若与基类中的函数原型出现不同,系统将根据不同情况分别处理:

(1)仅仅返回类型不同,其余相同,系统会当作出错处理;

(2)函数原型不同,仅仅函数名相同,系统会认为是一般的函数重载,将丢失虚特性。

当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类。

派生类构造函数的调用次序有三个原则:

1) 虚基类的构造函数在非虚基类之前调用;

2) 若同一层次中包含多个虚基类,这些虚基类的构造函数按它们说明的次序调用;

3) 若虚基类由非虚基类派生而来,则仍先调用基类构造函数,再调用派生类的构造函数。         
       重载函数在类型和参数数量上一定不相同,而重定义的虚函数则要求参数的类型和个数、函数返回类型相同;
虚函数必须是类的成员函数,重载的函数则不一定是这样;
构造函数可以重载,但不能是虚函数,析构函数可以是虚函数。

指针与数组名的区别

(1)数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组,也就是说其内涵是明确的;  

(2)数组名的外延在于其可以转换为指向其指代实体的指针,而且是一个指针常量;  

(3)指向数组的指针则是另外一种变量类型(在WIN32平台下,长度为4),仅仅意味着数组的存放地址!

而指针,不管是指向结构体、数组还是基本数据类型的指针,都不包含原始数据结构的内涵,在WIN32平台下,sizeof操作的结果都是4

 

许多程序员以为sizeof是一个函数,而实际上,它是一个操作符,不过其使用方式看起来的确太像一个函数了。语句sizeof(int)就可以说明sizeof的确不是一个函数,因为函数接纳形参(一个变量),世界上没有一个C/C++函数接纳一个数据类型(如int)为"形参"

 

4数据名作为函数形参时,其全面沦落为一个普通指针!它的贵族身份被剥夺,成了一个地地道道的只拥有4个字节的平民。

A.数组名作为函数形参时,在函数体内,其失去了本身的内涵,仅仅只是一个指针;
B.很遗憾,在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。

类成员的访问控制

可以实现信息隐藏,通过类和对象的封装:

1)在类中指定一些数据成员和函数成员为私有的,这就保证了程序运行的安全性。私有成员只有本类的函数成员或友元可以访问,其安全性可以保证,出了错也容易发现。

2)把函数与它所处理的数据联系到一起,使得程序中大量的操作﹑运算﹑处理等得到了最合理的划分,这种划分的合理性,其直接结果就是程序模块的可重用性加强了。

 

this 指针的概念

类的定义中,类的成员函数总是要对该类的数据成员进行处理。但类是 “虚”的,它只是一种抽象,一种设计,要处理的实际上是该类的对象。从另一方面说,类的函数成员的定义是说明性的,它的执行必须与该类的某一对象相联系。例如: cobj.print();

这就产生了一个问题:在定义函数成员时还没有具体的对象出现,但函数成员的定义中却往往要对它所将要依附的对象进行处理。(依赖this,对象创建后this自动指向当前对象)

为了解决这个问题, C++语言为类的定义设置了一个抽象的指针常量:this 指针。它无须用户的定义,相当于在类 C 的每一成员函数中隐含定义了:

const C* this

其特点是:

(1) this 是类 C 的指针,它指向类 C 的对象。

(2) this 是在该类的对象 cobj 被创建后,其成员函数 f()被调用时,this 也就同时被说明和创建,即

C cobj

cobj.f()相当于在 f()中有一说明

const C* this=&cobj;

在函数的运行过程中,指针 this 总代表着对象 cobj 的地址。即是说,可以

*this 表示该对象,也可用类似于 this->n 以及 this->print()来表示该对象的成

员。有时也使用 return this;表示返回该对象的指针。

 

类的静态数据成员

由于类的静态数据成员在一个类中只有一份拷贝,它并不为某一个类对象所特有,所以,对类的静态数据成员的访问通常使用<类名>::<静态数据成员名>”的方式。当然也可通过“<对象名>.<静态数据成员名>”的方式来进行指定,但由于只有一份拷贝,其中的<对象名>也只是起到一个类名的作用而已。

对不同的对象其值是一样的,相当于不同的对象拥有一个同步变化的变量。

类的静态函数成员

函数成员被说明成静态的,同样将与该类的不同对象无关。对类的静态函数成员的引用通常使用<类名>::<静态函数成员调用>”的方式(当然也可通过“<对象名>.<静态函数成员调用>”的方式)。

类的静态函数成员与类的非静态函数成员的最大区别在于:类的静态函数成员没有 this 指针,从而无法处理不同调用者对象的各自数据成员值。通常情况下,类的静态函数只处理类的静态数据成员值(它只隶属于类而不属于任一个特定对象)。若要访问类中的非静态成员时,必须借助对象名或指向对象的指针那样的函数参数。

对不同的对象其函数定义不能改变,但函数的返回值是可以改变的,因为函数可以带非静态参数。

类的常量数据成员

通过关键字 const 修饰的类中的数据成员。它不同于一般的符号常量,在成员说明时不能被赋值,而只能在对象被说明时通过构造函数的成员初始化列表的方式来赋初值。一旦对象被创建,其常量数据成员的值就不允许被修改,任何类内外函数只可读取其值,但不可改变它。

 

class CC{

int i;

const int c1;   //私有的常量数据成员 c1

public:

const int c2;    //公有的常量数据成员 c2

CC(int a,int b):c1(a),c2(b){

//成员初始化列表 c1(a)c2(b)将实参 a b 的值赋给 c1 c2

//很特别的语法格式。

i=c1;

};

};

类的常量函数成员 ?

类的函数成员也可以被说明为常量类型。常量类型的函数成员只有权读取相应对象(即调用者对象*this)的内容,但无权修改它们。

类的常量函数成员的说明格式如下:

<类型说明符> <函数名> ( <参数表> ) const;

要注意的是,修饰符 const 要加在函数说明的尾部(若放在首部的话,则是对函数值的修饰),它是函数类型的一部分。另外,在该函数的实现部分也要加 const 关键字。

常量型函数成员的说明有两个作用:

1)当函数体较大较复杂时,由系统帮助避免对对象内容的修改。

2)当对象被说明为常量对象时,可由系统限制,不让非常量函数成员被调用。

初始化符表:

即是说,若某个类中含有对象成员,则该类的构造函数就应包含一个初始化符表,用于负责对类中所含各对象成员进行初始化。在定义(生成)一个含有对象成员的类对象时,它的构造函数被系统调用,这时将首先按照初始化符表来依次执行各对象成员的构造函数,完成各对象成员的初始化工作,而后执行本类的构造函数体。析构函数的调用顺序恰好与之相反。

实现不同类对象的包含,还可以利用指向类对象的指针。

不直接以对象作为类的成员,而是以指向对象的指针作为类的成员,也是实现包含关系的常用方法。在这种情形下,由于对象指针是类的一般数据成员,所以不必如对象成员那样,在构造函数说明中加入成员初始化符表,而是直接在构造函数内进行初始化即可。

运算符重载

一般地说,单目运算符重载常选用成员函数方式,而双目运算符重载常选用友元函数方式。但不管选用哪种重载方式,对重载算符的使用方法则是相同。

在此,友元函数的定义用到类里面的成员时,必须带有一个类的对象作为形参。

继承与派生

1、 基类的友元关系和基类的构造函数和析构函数都不能被派生类所继承。构造函数不会继承,但可以通过派生获得基类构造函数的内容。

2、 虽然类 B 的的派生类 A 只可以存取类 B 的保护成员和公有成员,但是,派生类 A 的对象的创建,则必须包含着基类 B 的对象的创建。换句话说,派生类 A的对象包含了基类 B 的对象,这里面基类 B 的私有成员也在其中!

3、 即是说,构造派生类对象时,要对其基类数据成员、对其所含对象成员的数据成员以及其它的新增数据成员一块进行初始化。这种初始化工作是由派生类的构造函数来完成的(通过初始化符表)。

4、 当类的数据成员为另一个类的对象时,意味着包含,是指A类对象中总含有一个 B 类对象(对象成员)。它们属于整体与部分的关系(“has a”关系),如汽车和马达,马达是汽车的一部分。不妨称包含类对象的类为“组装类”。当使用类的继承产生派生类后,派生类对象中也总“拥有”基类的对象成员,这意味着派生类的对象必然是一个基类对象(“is a”关系),如汽车和轿车,首先轿车就是汽车(具有汽车的所有特征),另外它又比汽车有所特殊(还具有另外一些特殊属性)。

5、 <派生类对象>.<基类的公有或保护成员>

6、 通过“组装类”的类对象调用其对象成员的公有成员函数(或公有数据成员)时,则必须使用另外的调用方式:<组装类对象>.<对象成员>.<对象成员所属类的公有成员>

7、 前文已经指出,基类的友元不继承。即,如果基类有友元类或友元函数,则其派生类不因继承关系也有此友元类或友元函数。另一方面,如果基类是某类的友元,则这种友元关系是被继承的。即,被派生类继承过来的成员,如果原来是某类的友元,那么它作为派生类的成员仍然是某类的友元。

8、 具体地说,如果基类的静态成员是公有的或是保护的,则它们被其派生类继承为派生类的静态成员。即:(1)这些成员通常用“<类名>::<成员名>”方式引用或调用;(2)这些成员无论有多少个对象被创建,都只有一个拷贝。它为基类和派生类的所有对象所共享。

重载、超载、虚函数(虚函数是超载函数的一种)、静态联编、动态联编

    虚函数的使用与函数超载密切相关。若基类中某函数被说明为虚函数,则

意味着其派生类中也要用到与该函数同名、同参数表、同返回类型、但函数(实

现)体不同的这同一个所谓的超载函数。

虚函数和重载函数既有相似之处,又相互有区别。它们都是在程序中设置一组同名函数,都反映了面向对象程序中多态性特征,但虚函数有自己的特点:

1)虚函数不仅同名,而且同原型。

2)虚函数仅用于基类和派生类之中,不同于函数重载可以是类内类外函数。

3)虚函数需在程序运行时动态联编以确定具体函数代码,而重载函数在编译时即可确定。

4)虚函数一般是一组语义相近的函数,而函数的重载,可能相互是语义无关的。

另一点要指出的是:构造函数不能说明为虚函数。这是显然的,因为构造函数的调用一般出现在对象创建的同时或之前,这时无法用指向其对象(尚未创建)的指针来引用它。但析构函数可以说明为虚函数,此时这一组虚函数的函数名是不同的。当在析构函数中采用基类指针释放对象时,应注意把析构函数说明为虚函数,以确定释放的对象。

注意:C++规定,基类指针可以指向其派生类的对象(也即,可将派生类对象的地址赋给其基类指针变量), 但反过来不可以。这一点正是函数超载及虚函数用法的基础

在程序设计中利用虚函数和动态联编的方式,可以提高程序的水平和质量。是否能够在程序中充分地,正确地使用虚函数,是衡量一个C++程序员编程水平的标志之一。采用虚函数对于程序有益之处在于:

1)可使程序简单易读。例如,如果不如此处理上面讨论的实例,“pb->f()”肯定就要复杂的多;首先,在f()之前须增加类属限定符,显式地指明这里的 f()是指哪个类的函数成员;其次,由于当前pb 指针指向的对象是属于哪个类的,不一定可以简单确定,很可能需要若干条件判断语句来完成。

2)它使得程序模块间的独立性加强。例如有另一个类需要对各种图形(firgure)进行屏幕显示,通过虚函数的处理,它可以只与基类 figure 及其指针pb,函数成员 draw()直接打交道,而与 figure 的派生类rectanglecircletrianglesquare 等没有直接的联系。

3)增加了程序的易维护性。例如,在 figure 类的派生类中再增加一个派生类 ladder,除了增加类ladder的定义之外,有关调用虚函数draw()的程序不需要修改!

4)提高了程序中“信息隐藏”的等级。类的封装本身是私有成员的隐藏,而在基类与派生类之间虚函数的设置,实际上是以各派生类的基类为对外的接口,被隐藏的实际上是在各派生类中、其内容各异的实际处理。

在虚函数的原型后加上=0”字样而替换掉函数定义体(没有具体的实现),则这样的虚函数称为纯虚函数。例如:virtual void print()=0; 纯虚函数只为其派生类的各虚函数规定了一个一致的“原型规格”(该虚函数的实现将在它的派生类中给出)。含有纯虚函数的基类称为抽象基类。注意,不可使用抽象基类来说明并创建它自己的对象,只有在创建其派生类对象时,才有抽象基类自身的实例伴随而生。

初始化列表(基类,对象成员)

class line:public graphelem{             //派生类 line

pixel start, end;            //成员为 pixel类对象

public:

line(pixel sta, pixel en, colort col)

:graphelem(col),start(sta),end(en)   

{ };

 

 

 

Public/ protected/private

public:         具有这个访问控制级别的成员是完全公开的,任何环境下都可以通过对象访问这个成员   
private:       具有这个访问控制级别的成员是完全保密的,即只能通过指向当前类(不包括派生类)的   
                        this   指针才可以访问,其它环境均无法直接访问这个成员   
protected:   具有这个访问控制级别的成员是半公开的:外界无法直接访问这个控制级别的成员,但是派生类的   this   指针可以获得访问能力。   

 

保护成员不对外提供接口,也就不能通过对象访问,只能被类内部函数使用。

 

内联函数和宏定义的区别

#define Fun(x) ((x>5)&&(x<10)? (x):0)
int c = 7;
int d = Fun(++c);

 

inline int Fun(int x)
{
return ((x > 5) && (x < 10)) ? x : 0;
}
注意此时头文件要用名字空间形式
#include <iostream>
using namespace std;

 

内敛函数的功能和宏定义(#define )差不多,即在编译阶段进行替换,减少运行时间,提高运行效率。区别是宏定义没有语法检查,内敛函数有语法检查,更不会出错!

宏展开只是简单的文本替换,有表达式换表达式,有参数换参数,
int c = 7;
int d = Fun(++c);
这里应该换为( (++c>5)&&(++c<10)? (++c):0 )
执行结果应该是1010

但注意,如果改为内敛函数,这里结果应该是88.  因为函数是单向值传递而宏替换是简单的文本替换!

因为函数是单向值传递,替换后C就是常量7,而不是变量C,注意理解函数的值传递。

 

 

 

拷贝构造函数、浅拷贝、深拷贝

对于普通类型的对象来说,它们之间的复制是很简单的,例如:
int a=88;
int b=a; 
而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。下面看一个类对象拷贝的简单例子。 

#include <iostream>
using namespace std;

class CExample {
private:
    int a;
public:
    CExample(int b)
    { a=b;}
    void Show ()
    {
        cout<<a<<endl;
    }
};

int main()
{
    CExample A(100);
    CExample B=A;
    B.Show ();
    return 0;

运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象B分配了内存并完成了与对象A的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。下面举例说明拷贝构造函数的工作过程。

#include <iostream>
using namespace std;

class CExample {
private:
    int a;
public:
    CExample(int b)
    { a=b;}
    
    CExample(const CExample& C)
    {
        a=C.a;
    }
    void Show ()
    {
        cout<<a<<endl;
    }
};

int main()
{
    CExample A(100);
    CExample B=A;
    B.Show ();
    return 0;

CExample(const CExample& C)就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类X的拷贝构造函数的形式为X(X& x)

当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
一个对象以值传递的方式传入函数体 
一个对象以值传递的方式从函数返回 
一个对象需要通过另外一个对象进行初始化。

如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,后面将进行说明。

自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。

浅拷贝和深拷贝

  在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

 

 

文件、流

就输入输出操作来说,这些外设和硬盘上的文件是一致的,对于程序员来说文件只与信息的输入输出相关,而且这种输入输出是串行序列形式的。于是,人们把文件的概念抽象为“流”(stream)。

C++I/O 系统,定义了一系列由某种继承派生关系的流类,并为这些抽象的流类定义一系列的 I/O 操作函数,当需要进行实际的 I/O 操作时,只需创建这些类的对象(称为流),并令其与相应的物理文件(硬盘或软盘文件名或者外设名)相联系。

因此,文件可以说是个物理概念,而流则是一个逻辑概念。所有流(类对象)的行为都是相同的,而不同的文件则可能具有不同的行为。如,磁盘文件可进行写也可进行读操作;显示器文件则只可进行写操作;而键盘文件只可进行读操作。

在内存中开辟一片区域作为输入输出操作的缓冲区,可以提高运行效率,因此 I/O 操作可以区分为缓冲 I/O 和非缓冲 I/O

使用cout<<x;”的运算符调用方 式 (注意 , cout 为预定义的 ostream 类 对 象 ),完全等同于“cout.operator<<(x);”的函数调用方式。

由于算符重载函数operator <<”返回的是引用“ostream&”,可达到作为左值的目的,起到一个 ostream 类型的独立对象(变量)的作用,从而可知如下两种使用方式的合法性与等同性:

(cout.operator<<(x)).operator<<(y); //函数调用方式

cout<<x<<y; //运算符调用方式

 

头文件的定义(?)

如果这个函数的头文件只在一个源文件中调用编译器是不会报错的,如:
head.h
int max(int a, b){}

只在
source.cpp

#include "head.h"

但是在另一个源文件中也包含就会报错,即不能再出现如:
source2.cpp
#include "head.h"

解决办法,一是将函数移到cpp文件中,另一个方法是在h文件的函数定义前加 inline关键词。

函数定义写在头文件,被编译为内联函数。

 

C++动态内存分配

读者在使用动态变量时应注意的是,要保护动态变量的地址。例如在执行pi=new int;之后,不要轻易地冲掉指针 pi 中的值,假如执行了 pi=&a;语句之后,再释放原来生成的动态变量:

delete pi

已经达不到原来的目标了

 

 

Extern  static

 

staticC++中常用的修饰符,它被用来控制变量的存贮方式和可见性。extern "C"是使C++能够调用C写作的库文件的一个手段,如果要对编译器提示使用C的方式来处理函数的话,那么就要使用extern "C"来说明。

.C语言中的static关键字

C语言中,static可以用来修饰局部变量,全局变量以及函数。在不同的情况下static的作用不尽相同。

(1)修饰局部变量

一般情况下,对于局部变量是存放在栈区的,并且局部变量的生命周期在该语句块执行结束时便结束了。但是如果用static进行修饰的话,该变量便存放在静态数据区,其生命周期一直持续到整个程序执行结束。但是在这里要注意的是,虽然用static对局部变量进行修饰过后,其生命周期以及存储空间发生了变化,但是其作用域并没有改变,其仍然是一个局部变量,作用域仅限于该语句块。

在用static修饰局部变量后,该变量只在初次运行时进行初始化工作,且只进行一次。

:

1. #include<stdio.h>  

2. void fun()  

3. {   

4. static int a=1; a++;   

5. printf("%d\n",a);  

6. }  

7. int main(void)  

8. {   

9. fun();   

10. fun();   

11. return 0;  

12. }  

程序执行结果为: 2  3

说明在第二次调用fun()函数时,a的值为2,并且没有进行初始化赋值,直接进行自增运算,所以得到的结果为3.

对于静态局部变量如果没有进行初始化的话,对于整形变量系统会自动对其赋值为0,对于字符数组,会自动赋值为'\0'.

(2)修饰全局变量

对于一个全局变量,它既可以在本源文件中被访问到,也可以在同一个工程的其它源文件中被访问(只需用extern进行声明即可)。

:

1. //file1.c  

2. int a=1;  

3. file2.c  

4. #include<stdio.h>  

5. extern int a;  

6. int main(void)  

7. {  

8. printf("%d\",a);  

9. return 0;  

10. } 

则执行结果为 1

但是如果在file1.c中把int a=1改为static int a=1;

那么在file2.c是无法访问到变量a的。原因在于用static对全局变量进行修饰改变了其作用域的范围,由原来的整个工程可见变为本源文件可见。

(3)修饰函数

static修饰函数的话,情况与修饰全局变量大同小异,就是改变了函数的作用域。

.C++中的static

C++中static还具有其它功能,如果在C++中对类中的某个函数用static进行修饰,则表示该函数属于一个类而不是属于此类的任何特定对象;如果对类中的某个变量进行static修饰,表示该变量为类以及其所有的对象所有。它们在存储空间中都只存在一个副本。可以通过类和对象去调用。

.extern关键字

C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。
在上面的例子中可以看出,在file2中如果想调用file1中的变量a,只须用extern进行声明即可调用a,这就是extern的作用。在这里要注意extern声明的位置对其作用域也有关系,如果是在main函数中进行声明的,则只能在main函数中调用,在其它函数中不能调用。其实要调用其它文件中的函数和变量,只需把该文件用#include包含进来即可,为啥要用extern?因为用extern会加速程序的编译过程,这样能节省时间。

C++中extern还有另外一种作用,用于指示C或者C++函数的调用规范。比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同,用此来解决名字匹配的问题。

 

 extern不与"C"在一起修饰变量或函数时,如在头文件中: extern int g_Int; 它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块或者其他模块中使用记住它是一个声明不是定义!也就是说B模块(编译 单元)要是引用模块(编译单元)A中定义的全局变量或函数时,它只要包含A模块的头文件即可, 在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。

    如果你对以上几个概念已经非常明白的话,那么让我们一起来看以下几种全局变量/常量的使用区别:

1. extern修饰的全局变量
    以上已经说了extern的作用,下面我们来举个例子,
    test1.h中有下列声明:
    #ifndef TEST1H
    #define TEST1H
   extern char g_str[]; // 声明全局变量g_str
    void fun1();
    #endif
    test1.cpp
    #include "test1.h"
    
    char g_str[] = "123456"; // 定义全局变量g_str
    
    void fun1()
    {
        cout << g_str << endl;
    }
    
    以上是test1模块, 它的编译和连接都可以通过,如果我们还有test2模块也想使用g_str,只需要在原文件中引用就可以了
    #include "test1.h"

    void fun2()
    {
        cout << g_str << endl;
    }
    以上test1test2可以同时编译连接通过,如果你感兴趣的话可以用ultraEdit打开test1.obj,你可以在里面着"123456"这 个字符串,但是你却不能在test2.obj里面找到,这是因为g_str是整个工程的全局变量,在内存中只存在一份, test2.obj这个编译单元不需要再有一份了,不然会在连接时报告重复定义这个错误!
    有些人喜欢把全局变量的声明和定义放在一起,这样可以防止忘记了定义,如把上面test1.h改为
    extern char g_str[] = "123456"; // 这个时候相当于没有extern
    然后把test1.cpp中的g_str的定义去掉,这个时候再编译连接test1test2两个模块时,会报连接错误,这是因为你把全局变量 g_str的定义放在了头文件之后,test1.cpp这个模块包含了test1.h所以定义了一次g_str,test2.cpp也包含了test1.h所以再一次定义了g_str, 这个时候连接器在连接test1test2时发现两个g_str如果你非要把g_str的定义放在test1.h中的话,那么就把test2的代码 中#include "test1.h"去掉 换成:
    extern char g_str[];
    void fun2()
    {
        cout << g_str << endl;
    }
    这个时候编译器就知道g_str是引自于外部的一个编译模块了,不会在本模块中再重复定义一个出来,但是我想说这样做非常糟糕,因为你由于无法在 test2.cpp中使用#include "test1.h", 那么test1.h中声明的其他函数你也无法使用了,除非也用都用extern修饰,这样的话你光声明的函数就要一大串,而且头文件的作用就是要给外部提 供接口使用的,所以 请记住, 只在头文件中做声明,真理总是这么简单

 

 

 

 

 


猜你喜欢

转载自blog.csdn.net/u010202588/article/details/9967955