C++基础知识

来源:互联网 发布:单片机控制3.3v 编辑:程序博客网 时间:2024/06/01 10:30

1      基本知识

1.1    运算符

(1)new最基本的两种用法:

①  开辟单变量地址空间 

Ø  int *p= new int ;  //p指向一个int 型空间 

Ø  int *p = new int(5) ; //作用同上,但是同时将整数赋值为5

Ø  Student *p=new Student; //Student 为类名,且有默认构造函数

Ø  Student *p=new Student(‘a’); //Student为类名,利用构造函数初始化对象

②    开辟数组空间 

Ø  一维: int *a = new int[100];开辟一个大小为100的整型数组空间

Ø  二维: int *a = new int[5][6]

Ø  三维及其以上依此类推.

    注:new运算符为数组分配空间时,不能为数组进行初始化。

一般用法: new 类型 [初值] 

(2)delete用法: 

Ø  int *a = new int; 

delete a;   //释放单个int的空间

Ø  int *a = new int[5]; 

delete []a; //释放int数组空间

         程序中使用new运算符在堆中开辟一个空间,当数据使用完后,一定要利用delete运算符释放在堆中开辟的空间,否则会出现内存泄漏。

         在程序中使用new运算符分配空间时,不要轻易修改指针指向的地址。因为当前指针指向了堆空间的地址,如果将其再指向其他地址,则导致无法释放堆空间,造成内存泄漏。

         注:1)可利用CRT调试功能检测内存泄漏。

2)内存泄漏指用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元,直到程序结束。内存泄漏形象的比喻是“操作系统可提供给所有进程的存储空间正在被某个进程榨干”,最终结果是程序运行时间越长,占用存储空间(虚拟内存)越来越多,最终用尽全部存储空间,整个系统崩溃。由程序申请的一块内存,如果没有任何一个指针指向它,那么这块内存就泄漏了。

1.2    回调函数

回调函数一般用于截获消息、获取系统信息或处理异步事件。回调函数类似于一个中断处理函数,应用程序将回调函数的地址指针告诉,在符合设定的条件时,DLL会自动调用该函数。回调函数的特点如下:

Ø  回调函数是由开发者按照一定的原型进行定义的函数。

Ø  回调函数并不是由开发者直接调用执行。

Ø  回调函数通常作为参数传递给系统API,由该API来调用。

Ø  回调函数可能被系统API调用一次、也可能被循环调用多次。

为了使用回调函数,需要做以下工作:1)声明函数原型;2)定义回调函数;3)设置触发条件,即在函数中将回调函数名称转化为一个作为参数的地址,以便于DLL调用。

使用回调函数的示例程序代码如下:

…//此处代码省略

typedefint(*CallBack)(char *p);               //声明CallBack类型的函数指针

int A(char *p)

{

         AfxMessageBox(“A”);

         AfxMessageBox(p);

         return 0;

}

int B(CallBacklpCall,char *p)

{

         AfxMessageBox("B");

         AfxMessageBox(p);

         lpCall(p);  //借助回调完成的功能,即A函数来处理的

         return 0;

}

void hello::OnButton9()

{

         char *p="hello!";

         B(A,p);

}

…//此处代码省略

1.3    数组与指针

(1)指针数组

(2)数组指针

         数组指针,即指向一维数组的指针变量。其定义格式如下:

                   <类型> (*<指针变量名>)[<数组列数>];

         其中,(*<指针变量名>)定义了指向一维数组的指针变量,该指针变量可指向二维数组中的某行。例如:

         inta[3][3]={{1,2,3},{4,5,6},{7,8,9}};

         int(*p)[3];

         p=&a[0];

         这样,则可用(*p)[0]、(*p)[1]、…(*p)[n-1]来表示数组a第i行的元素a[i][0]、a[i][1]、…a[i][n-1]。

         当然,也可以用*(*p+0)、*(*p+1)、…*(*p+n-1)来表示数组a第i行的元素a[i][0]、a[i][1]、…a[i][n-1]。

         注:*p两侧的括号不可缺少,如果写成*p[3],由于方括号[]运算级别高,因此p先与[3]结合,表示数组;然后再与前面的*结合,*p[3]是指针数组

(3)指针与二维数组

         编译程序在为二维数组分配连续内存空间时,是按二维数组的行来逐行存放数组元素的。可以把二维数组的每一行看成是一个元素。例如:

int a[4][3];

         可以看成是数组a包含4个元素,分别是a[0]、a[1]、a[2]、a[3],这四个元素是一维数组,因此这4个元素又分别表示一维元素的起始地址,即指针。

         数组名用来代表数组的起始地址。对于二维数组来说,数组名a代表了第0行第0列的元素地址,也就是数组第0行的首地址,即a=& a[0][0]=&a[0]。a[i]就可表示第i行第0列元素的起始地址。一维数组的第i个元素的地址可表示为:数组名+i。因此一维数组a[i]中第j个元素a[i][j]的地址(&a[i][j])可表示为:a[i]+j。它的值可表示为*( a[i]+j)。

为了区分元素地址与行地址,C++中规定:

Ø  第一行的起始地址(行地址)用a+i或&a[i]表示;

Ø  第i行第0列元素的起始地址(元素地址):用a[i]或*(a+i)表示。

因此,第i+j行的起始地址就可表示为:a+i+j,&a[i]+j。第i行第j列元素的地址就可表示为:*(a+i)+j、a[i]+j、&a[i][j]、&a[i]+j。第i行第j列元素的值在前面加上取内容元素符*即可。如下表所示为二维数组的行地址与元素地址的各种表示方法及含义。

表.用数组名表示数组元素的行地址、元素地址和元素的值

表示方法

含义

a

二维数组名,数组的起始地址,数组第0行的地址

a[0]

第0行第0列元素的起始地址

a+0

第0行的起始地址

*a、*(a+0)

第0行第0列元素的地址

**a、**(a+0)、*a[0]、*(*(a+0)+0)

元素a[0][0]

a+i、&a[i]

第i行的起始地址

a+i+j、&a[i]+j

第i+j行的起始地址

a[i]、*(a+i)、&a[i][0]

第i行第0列元素的地址

*(a+i)+j、a[i]+j、&a[i][j]、*&a[i]+j

第i行第j列元素的地址

*(*(a+i)+j)、a[i][j]、*(a[i]+j)、*(&a[i][j])、*(*&a[i]+j)

第i行第j元素的值

示例代码如下:

#include<iostream.h>

int main()

{

         inta[4][3]={{00,01,02},{10,11,12},{20,21,22},{30,31,32}};

         int (*p)[3]=a;                      //定义指向一维数组的指针变量

         for(intx=0;x<4;x++)

         {

                   cout<<"a["<<x<<"][2]:"<<'\t'<<*(*p+1)<<endl;//显示第二列

                   p++;

         }

         cout<<"a:"<<'\t'<<'\t'<<a<<endl;

         cout<<"a[0]:"<<'\t'<<'\t'<<a[0]<<endl;

         cout<<"&a[0]:"<<'\t'<<'\t'<<&a[0]<<endl;

         cout<<"&a[0][0]:"<<'\t'<<&a[0][0]<<endl;  // a,a[0],均指向第0行第0列元素

         return 0;

}

2      封装

2.1    构造函数和析构函数

(1)复制构造函数

         当需要创建多个相同的对象时,可以采用复制构造函数,同默认构造函数一样,编译器提供默认的复制构造函数。示例代码如下:

#include<iostream.h>

class Pen

{

public:

         Pen(int size);

         Pen(Pen &pen);        //声明一个复制构造函数

         int GetSize();

         void Write();

private:

         int m_size;

};

Pen::Pen(int size)

{

         m_size=size;

}

Pen::Pen(Pen &pen)         //复制构造函数的定义

{

         m_size=pen.GetSize();

         cout<<"sizeand long";

}

int Pen::GetSize()

{

         returnm_size;

}

void Pen::Write()

{

         cout<<"thevalue of m_size is "<<GetSize()<<endl;

}

 

int main()

{

         Penpen1(10);

         pen1.Write();

         Penpen2(pen1);       //调用复制构造函数

         pen2.Write();

         return0;

}


(2)析构函数没有参数,所以析构函数是不能重载的。一个类可以有很多个构造函数,但是只能有一个析构函数。如果在撤销对象时,不需要做任何工作,可以不定义析构函数。

(3)构造函数和析构函数的调用顺序

         通常情况下,创建对象时调用构造函数,在撤销对象时调用析构函数。最先被调用的构造函数,其对应的析构函数最后被调用;最后被调用的构造函数,其对应的析构函数最先被调用。

注:exit()函数和abort()函数的功能是终止程序的执行。当执行exit()函数时,系统要做终止程序前的收尾工作,即调用析构函数;而执行abort()函数时,直接终止程序的执行。因此,在构造函数中要终止程序的执行,不能使用exit(),这样会形成无限递归,但是可以使用abort()函数。

2.2    友元函数

友元不是成员函数,但是它可以访问类中的私有成员。友元的作用在于提高程序的运行效率。但是,它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。友元函数的形参一般是对象的引用。

利用友元函数访问类中的私有成员变量的示例代码如下:

#include<iostream.h>

class TestFriend

{

public:

         void set(int i,int j);

         friend voidget(TestFriend &tf);                 //友元函数的声明

private:

         int x,y;

};

void TestFriend::set(int i,int j)

{

         x=i;

         y=j;

}

void get(TestFriend &tf)                              //友元函数的定义

{

         cout<<tf.x<<"  "<<tf.y<<endl;

}

void main()

{       

         TestFriend tf;

         tf.set(1,2);

         get(tf);

}

3      继承

(1)派生类的构造函数

构造函数不能被继承,因此派生类的构造函数除了对自己的数据成员进行初始化外,还必须通过调用基类的构造函数来初始化派生类的基类成员。如果派生的构造函数只在类的主体中声明,而在类的主体外定义,那么在声明时就不包括基类构造函数名及其参数表,只有在定义函数时才列出。

派生类构造函数的定义中可以省略对基类构造函数的调用,其条件是在基类中必须有默认的构造函数,或者根本没有定义构造函数。当然,基类中没有定义构造函数,派生类根本不必负责调用基类的构造函数。

(2)派生类的析构函数

         析构函数也不能被继承,它也需要在派生类的析构函数调用基类的析构函数。派生类中定义的析构函数用来撤销派生类中新增加的成员。基类中的成员仍然由基类的析构函数来处理。

(3)覆盖成员函数

         在派生类中重新定义一个在基类中已经定义的成员函数,那么派生类对象调用的是派生类重新定义的成员函数。也就是说,如果派生类中声明一个成员函数,这个成员函数与基类中的一个成员函数相同。定义一个派生类的对象,通过这个对象调用这个成员函数,那么访问的是派生类中的成员函数。

(4)多重继承的二义性

         若基类A中有数据成员a, B与C由A派生而来,当D继承基类B与C时,编译系统无法确定访问哪个基类的数据成员a,出现二义性。

         可以通过虚基类来解决。为了在派生类中保留一份基类的成员,该基类的所有直接派生类中都应当声明为虚基类。声明虚基类的格式如下:

         class派生类名:virtual 继承方式基类名

         {…}

         C++中规定:编译系统只执行最后一个派生类对虚基类的构造函数的调用,而在执行其他派生类的构造函数时,都不调用虚基类的构造函数。因此,类D的构造函数直接调用虚基类的构造函数,而类B和类C都不调用类A的构造函数。在派生类D的构造函数中,没有给出对虚基类A的调用,因此就调用虚基类A的默认构造函数,所以下面示例代码中,输出x的值为0。如果类D构造函数的初始化列表中添加调用虚基类A的构造函数:

         D(inta,int b,int d,int e,int f):A(1),B(a,b),C(d,e)

则输出x的值就为1。示例代码如下:

#include<iostream.h>

class A

{

public:

         intx;

         A(inta=0){x=a;}

};

class B:virtual public A

{

public:

         inty;

         B(inta,int b):A(b)

         {y=b;}

         voidshow()

         {cout<<"x="<<x<<'\t'<<"y="<<y<<'\n';}

};

class C:virtual public A

{

public:

         intz;

         C(inta,int b):A(b)

         {z=a;}

         voidshows()

         {cout<<"x="<<x<<'\t'<<"z="<<z<<'\n';}

};

class D:public B,public C

{

public:

         intm;

         D(inta,int b,int d,int e,int f):B(a,b),C(d,e)

         {m=f;}

         voidprint()

         {

                   show();

                   shows();

                   cout<<"m="<<m<<'\n';

         }

};

int main()

{

         Dd(1,2,3,4,5);

         d.print();

         return0;

注:并不是所有的面向对象的程序设计语言都支持多重继承,例如Java和Smalltalk等。因为多重继承比较复杂,程序的编写、调试和维护工作都很困难,容易产生二义性。许多专业人员并不提倡在程序中使用多重继承。

4      多态

多态是指不同的对象在接收同一消息时可以产生完全不同的行为。多态的重要性在于允许一个类体系的不同对象,可以以各自不同内容的函数响应同一消息,实现“同一个接口,多种方法”。

多态的支持可分为两种,静态联编支持静态多态,它是通过函数重载和运算符重载来实现。动态联编支持动态多态,它是通过虚函数来实现的。

(1)运算符的重载

(2)虚函数

         在基类把某个成员函数定义为虚函数后,允许在派生类中重新定义该函数,以实现新的功能。如果没有重新定义,派生类只能简单地继承基类的虚函数。需要注意的是:

Ø  虚函数一定是类的成员函数,因为虚函数仅适用于有继承关系的类对象。

Ø  虚函数一定是非静态的成员函数,因为静态的成员函数不受类对象的限制。

Ø  构造函数不能是虚函数,但是通常将析构函数定义为虚函数,以便实现撤销对象时的多态。

Ø  在实现这种动态多态时,必须使用基类的指针或引用。

Ø  在调用虚函数时,执行的速度要比一般成员函数慢,因为如果一个类中含有虚函数时,编译系统就会为类构造虚函数表,用来保存相应虚函数的入口地址,函数的调用是间接实现的。

(3)虚析构函数

         在撤销派生类对象时,一般先调用派生类的析构函数,然后再调用基类的析构函数。但是,如果定义了一个指向基类的指针,该指针又指向用new运算符创建的派生类对象,并且基类又有析构函数。则在程序中,用运算符delete撤销对象时,系统只会执行基类的析构函数,而不会执行派生类的构造函数。为了能够保证正确地执行析构函数,就需要定义虚析构函数,这样派生类的析构函数和基类的析构函数都会调用。示例代码如下:

#include<iostream.h>

class A

{

public:

         virtual~A(){cout<<"调用A的析构函数。\n";}

};

class B:public A

{

public:

         B(inti){buf=new char[i];}

         ~B()

         {

                   delete[]buf;

                   cout<<"调用B的析构函数。\n";

         }

private:

         char*buf;

};

void fun(A *a)

{

         deletea;

}

 

int main()

{

         A*a=new B(15);

         fun(a);

         return0;

}


程序的运行结果:

调用B的析构函数。

调用A的析构函数。

 

从结果中可以看到,先调用了派生类的析构函数,然后调用了基类的析构函数,如果将基类A中的析构函数不定义成虚析构函数,输出结果就为:

调用A的析构函数。

注:如果一个基类的析构函数被声明为虚析构函数,则它的派生类的析构函数也是虚析构函数,不管它是否使用了关键词virtual,也不管它的函数名是否相同。

(4)纯虚函数

         在声明基类时,可能会遇到这样的情况:无法定义基类中虚函数的功能,但是它的派生类都需要使用该虚函数的功能。此时,就可以将基类中的虚函数声明为纯虚函数。纯虚函数是一种特殊的虚函数,声明格式如下:

         virtual类型 函数名(参数)=0;

         纯虚函数应当注意以下几点:

Ø  纯虚函数没有函数体;

Ø  最后面的“=0”并不代表函数的返回值为0,它只是一种形式,通知编译系统这是一个纯虚函数。

Ø  不能调用纯虚函数。它只是声明这是一个虚函数,具体的实现在派生类中定义。在派生类中对此函数声明后,就可以调用它。

Ø  如果在基类中声明了纯虚函数,而在其派生类中没有对该函数进行声明,则该函数在派生类中仍是纯虚函数。

纯虚函数的示例代码:

#include<iostream.h>

class Virtualbase

{

public:

         virtualvoid Demon()=0;

         virtualvoid Base(){cout<<"this is farther class"<<endl;}

};

 

class SubVirtual:public Virtualbase

{

public:

         voidDemon(){cout<<"this is SubVirtual"<<endl;}

         voidBase(){cout<<"this is subclass Base"<<endl;}

};

 

void main()

{

         Virtualbase*inst=new SubVirtual;

         inst->Demon();

         inst->Base();

}


         注:虚函数和纯虚函数的区别

当定义为纯虚函数时,在子类中必须实现,否则子类还是抽象类;定义为虚函数时,可以不必在子类中实现,由父类指针指向的子类默认执行父类中的代码,当该子类需要另外的实现时,可以重写该虚函数。(如果不定义为虚函数,即使重写,由于指针为父类型,只能执行父类中的代码)

 

0 0
原创粉丝点击