结构体
1定义变量:
1.定义同时顺带定义变量
struct{
c语言只允许结构体含数据
c++增加了,允许如同类一样的功能
}student1,student2;
2.定义后再定义变量
struct Student{....
};
Student student1,student2;
struct Student student1
非法范例
1.
struct{
}Student;
Student a;
2.结构体类型只是一种数据类型,,用户可以自行定义内容,其不占内存空间,只有定义结构体类型变量时才开辟内存空间
Struct student student1, student2;开辟了空间
3.#define在结构体中的应用:替换
# define STUDENT struct Student//标识符等价struct Student
STUDENT
{ int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
STUDENT student1,student2;
4.结构体类型的变量在内存依照其成员的顺序排列,所占内存空间的大小是其全体成员所占空间的总和。
5.结构体的成员可以是另一个结构体类型
struct student
{ int num;
char name[20];
struct date birthday;//类似与类中的“组合”知识点
};
6.不能对结构体变量整体赋值或输出,只能分别对各个成员引用。
cin>>student1;非法
cin>>student1.name;正确
7.可以将一个结构体变量整体赋给另外一个相同类型的结构体变量。 student2=student1;
8.结构体成员相当于变量,student.name 相当于一个变量
9.结构体变量初始化
void main(void)
{ struct student
{ long int num;
char name[20];
char sex;
char addr[30];
} student1={901031, “Li Lin”, ‘M’, “123 Beijing Road”};
cout<<student1.name<<endl;
10.结构体变量可以作为函数的参数,函数也可以返回结构体的值。当函数的形参与实参为结构体类型的变量时,这种结合方式属于值调用方式,即属于值传递。
结构体数组
1.初始化
1.
struct student
{ int num;
char name[20];
char sex;
} stu[3]={ {1011, "Li Lin",'M'}, {1012,"Wang Lan",'F'},
{1013,"Liu Fang",'F'};
2.
struct student
{ int num;
char name[20];
char sex;
} stu[ ]={ {1011,"Li Lin",'M'}, {1012,"Wang Lan",'F'},
{1013,"Liu Fang",'F'}};
结构体静态类型
1.当把结构体类型中的某一个成员的存储类型定义为静态时,表示在这种结构类型的所有变量中,编译程序为这个成员只分配一个存储空间,即这种结构体类型的所有变量共同使用这个成员的存储空间
2.静态类型在编译的时候已经分配了空间
struct Student{
static int id;静态成员
int eng;
};
int Student::id=50;
3.在结构体中说明的静态成员属于引用性说明,必须在文件作用域中的某一个地方对静态的成员进行定义性说明,且仅能说明一次
int Student::id=50;
指向结构体变量的指针
1.三种赋值方式
Student stu; // 定义student类型的变量
Student *p = &stu; // 定义指向student类型的指针p
// -> 指向结构体变量的运算符
1.Stu.name = “xx”;
2.(*p).name = “xxx”;
3.p->name = “xxxx”;
2.指向结构体数组的指针
struct student
{
int num;
float score;
}stu[3]={{110,1},{111,2},{113,3}},*p;
p=stu;
p->num 正确引用
(p++).num 非法引用
(p++)->num 正确引用
(*p).num正确引用
new
1.new 相当于一个函数,在内存开辟完空间后,返回这个空间的首地址,这时,这个地址必须用一个指针保存下来,才不会丢失。
2.解决无法动态创建空间的缺点
int a;
cin>>a;
int str[a];非法,无法动态创建数组空间
3.用new动态创建空间
int n,* p;
cin>>n;
p=new int[n];正确
4.注意:用new开辟的内存单元没有名字,指向其首地址的指针是引用其的唯一途径,若指针变量重新赋值,则用new开辟的内存单元就在内存中“丢失”了,别的程序也不能占用这段单元,直到重新开机为止。
int * p, a[4];
p=new int[4];
p=a; p原来指向的空间再也没法被使用
5.用 new 运算符分配的空间,不能在分配空间时进行初始化。
6.在此期间,point指针不能重新赋值,只有用new开辟的空间才能用delete收回。
int *point;
point=new int;
......
delete point;
7.delete也可以收回用new开辟的连续的空间。
int *point;
cin>>n;
point=new int[n];
.......
delete [ ]point;
8.当内存中没有足够的空间给予分配时,new 运算符返回空指针NULL(0)。
结构体创建链表
struct student
{ int num;
float score;
struct student *next;
} ;
或
#define STU struct student
STU
{ int num;
float score;
STU *next;
} ;
看这个图,很清晰
循环法
结束的地方也是开始的地方
输出链表
void print(STU * head)
{ STU *p;
p=head;
while(p!=NULL)循环条件是移动指针不为空
{ cout<<p->num<<‘\t’<<p->score<<‘\n’;
p=p->next;
}
}
删除节点
1、首先定义两个结构体类型的指针 STU *p1, *p2;
2、将链表的表头赋给p1, p1=head;
3、判断p1所指向的结点是否是要删除的结点 p1->num a1。
4、若p1->num!=a1, p2=p1; p1指向下一个结点p1=p1->next,继续判断下一个结点是否是要删除的结点。继续做3。5、若p1->num= =a1,则p1当前指向的结点就是要删除的结点,将p2的指针成员指向p1所指的下一个结点。若为new需再delete空间。
特殊情况
1、若链表为空链表,返回空指针
2、删除的结点为头结点时,head指向下一个结点
3、链表内没有要删除的结点,返回提示信息。
插入节点
插入结点:要插入结点的链表是排序的链表。插入10
1、定义三个结构体指针变量 STU *p1,*p2,*p0; p0指向要插入的结点。p1=head;
2、比较p1->num 与p0->num,若p1->num< p0->num,p2=p1; p1=p1->next; 继续比较。
3、若p1->num> =p0->num,p0应插在p1与p2之间,则p2-next=p0 ; p0->next=p1;
特殊情况:
1、若链表为空链表,将插入结点作为唯一的结点,head=p0;返回。
2、若插入结点中的数据最小,则插入的结点作为头结点。
p0->next=head;
head=p0;
3、插入到链尾,插入结点为最后一个结点。
p2->next=p0;
p0->next=NULL;
student *insert1(student*head,student*stud){//传进链表,和所插入的节点
student *p1=head,*p2;//p1用来指向后节点,p2指向前节点
if(head->numbers==NULL)//,链表为空,那么插入的节点就是第一个节点,首指针直接指向它;
{
head=stud;
head->next=NULL;//只有一个节点,没有下一个节点
}
else{
while(stud->numbers>p1->numbers&&p1->next!=NULL){
//按学号从小到大找到所插入节点的后一个节点,或整个链表没有比他大的了p1指针直接指向了最后节点
//循环条件:插入节点序号大于p1指针,或下一节点非空
p2=p1;//p2指针解放p1指针
p1=p1->next;//p1指向下一个指针
}
//到这里,p1指向所插入节点的后一节点或同时也是指向最后一节点
if(stud->numbers<=p1->numbers)//在链表中找到插入节点的后一节点,可能插入节点的值大小与所在节点一致
{
if(head==p1) head=stud;// 刚好插入的节点是首节点
else p2->next=stud;//插入节点不是首节点,那么插入节点的前一个节点指向它
stud->next=p1;//再插入节点指向它的后节点
}else//否则,链表中找不到比插入节点大的节点,所以插入节点插入到链表最后
{
p1->next=stud;
stud->next=NULL;
}
}
return (head);
}
main
枚举
1.如果一个变量只有几种可能的值,可以定义为枚举类型。
2.枚举类型就是将变量的值一 一列举出来,变量的值仅限于列举出来的值的范围内。
3.枚举元素为常量,不可赋值运算。 sun=0; mon=1;
4.在定义枚举类型的同时,编译程序按顺序给每个枚举元素一个对应的序号,序号从0开始,后续元素依次加1
5.只能给枚举变量赋枚举值,若赋序号值,则必须进行强制类型转换。
day=mon ; day=(enum weekday)1;
6.枚举元素可以用来进行比较判断。
if (workday= = mon) , if (workday>sun)
7.枚举值可以进行加减一个整数n的运算,得到其前后第n个元素的值。
workday=(week)(workday+2)
8.枚举值可以按整型输出其序号值。
多态、封装、继承
多态
1.多态的两种形式:虚函数和函数重载
类
1.类的含义
1 类是c++语言的数据抽象和封装机制,它描述了一组具有相同属性(数据成员)和行为特征(成员函数)的对象。 是代码复用的基本单位,也是一种数据类型
2. 对象是类的实例,类就像数据类型而对象就像是变量。
3.一个类的对象只有一个(判断:正确)
4.类:代表了某一批对象的共性和特征。类是对象的抽象,而对象是类的具体实例(instance)5.类是一种复杂的数据类型,它是将不同类型的数据和与这些数据相关的运算封装在一起的 集合体。
2.类的定义
class 类名
{
private: 放 私有数据成员 和 成员函数
public: 放 公有数据成员 和 成员函数
protected: 放 受保护的数据成员 和成员函数
}
-
私有成员private:①只能有本类的成员函数或者特殊说明的函数(比如通过公共函数作为接口)才能访问,②不能通过对象直接访问,③private成员处于类的第一部分可以省略private关键字,未指明那种访问权限时,默认是private;若要访问对象的私有的数据成员,只能通过对象的公有成员函数来获取
-
受保护成员protect:只能由该类的成员函数,友元,公有派生类成员函数访问;(与私有区别在于继承性)
-
公有成员:对外完全开放,可以类外直接使用
-
数据成员可以是任何数据类型,但不能用自定(auto),寄存器(register)或 extern 进行说明
-
定义类时,不允许初始化数据类型,下例是错误示范:
class A
{
private:
int n = 0;//非法
int m = 5;//非法
};
- 结构体和类的区别:
①结构体只有数据成员,无成员函数(c语言是不行的,但c++可以),类则都有
②结构体数据都是公开的,类则分为三种 有数据保护作用
③ 结构体中,数据和其相应的操作是分离的是程序的复杂性难以控制,复用性不好
一般仅有数据就用结构体,有数据又有操作的成员函数则用类
④结构体类型只是类的一个特例。结构体类型与类的唯一的区别在于:在类中,其成员的缺省的存取权限是私有的;而在结构体类型中,其成员的缺省的存取权限是公有的。 - 类的成员不能有自身对象,但可以有自身类对象的指针,或者自身类对象的引用,也可以另一个类的对象
- 类中的任何成员数据均不能使用关键字extern,auto或register限定其存储类型
- 在定义类时,只是定义了一种导出的数据类型,并不为类分配存储空间,所以,在定义类中的数据成员时,不能对其初始化
class Test {
int x=5,y=6; //是不允许的
extern float x; //是不允许的
Test a;//是不允许的
}
3.类的成员函数
1.外联成员函数定义,定义格式:
class Tdate
{
public:
void set(int m, int d, int y);
int year();
private:
int month;
int day;
int year;
};//这里注意加分号
void Tdate:: set(int m, int d, int y) //外部定义加上作用域:
{
month = m; day= d; year=y;
}
2.作用域运算符::的前面没有类型,则指它不属于任何类,而是全局函数
3.类体必须在函数定义的位置之前
4.inline内联成员函数,可以外部也可以内部定义,同样效果
class Tdate
{
public:
inline set(int m, int d, int y)
{//inline也可以省略
month= m,day = d, year = y;
}
private:
int month;
int day;
int year;
};//这里注意加分号
也可以在外部定义
inline void Tdate:: set(int m, int d, int y)
区别:外联有利于整个程序的模块化设计,内联避免函数调用机制所带来的开销,提高程序的执行效率
5.成员函数不能是静态的
6.类的成员函数指针可以访问类中的任何一个成员函数(判断:错误)
7.一个类中的成员函数不能是另一个类的成员
4.模块化设计
模块化设计是信息隐蔽的重要思想,信息隐蔽对开发大的程序非常有用,可以在极大的程度上保证程序的质量。
tdate.h 文件值放 Tdate类的定义说明
#ifndef Tdate
#define Tdate
class Tdate
{
public:
void set(int m, int d, int y);
int year();
private:
int month;
int day;
int year;
};
#endif
前两行和最后一行的作用是如果一个程序多个文件均包含Tdate 类,则在编译时可以避免Tdate类中标识符的重复定义。
#endif 之前的所有行将直接跳过,除了第一次定义外,以后每次编译器遇到以下的编译预处理命令(过程):#include“tdate.h”
则以#ifndef 开始的命令测试标识符 Tdate 是否已经定义,若未定义,则第二行则进行定义且赋值为NULL;如果以后再一次包含了“tdate.h”则编译器判断第一行#ifndef 来判断已经定义了,则直接跳过避免重复定义
5.类的作用域
1.类类型的作用域:在函数(如main函数)定义之外定义的类,其类名的作用域为文件作用域;而在函数体内定义的类,其类名的作用域为块作用域 。
6.类是允许嵌套定义的
1.在类A的定义中,并不为b1,b2分配空间,只有在定义类A的对象时,才为嵌套类的对象分配空间。嵌套类的作用域在类A的定义结束时结束。
class A {
class B{
int i,j;
public :
void Setij(int m, int n){ i=m; j=n; }
};//b类到这里
float x,y;
public:
B b1,b2;//暂时还不分配空间
void Setxy( float a, float b ){ x=a; y=b;}
void Print(void) {cout<<x<<‘\t’<<y<<endl; }
};
对象
概念
1.概念:对象是由一组属性和一组行为构成的。
2.使用面向对象的程序设计方法设计一个复杂的软件系统时,首要的问题是确定该系统是由哪些对象组成的,并且设计这些对象
3.把对象的内部实现和外部行为分隔开来
4.面向过程程序设计:是围绕功能进行的,用一个函数实现一个功能。所有的数据都是公用的
5.面向对象程序设计:采取的是另外一种思路。它面对的是一个个对象。实际上,每一组数据都是有特定的用途的,是某种操作的对象。也就是说,一组操作调用一组数据
6.程序设计者的任务包括两个方面:一是设计所需的各种类和对象,即决定把哪些数据和操作封装在一起;二是考虑怎样向有关对象发送消息,以完成所需的任务。各个对象的操作完成了,整体任务也就完成了
7.因此人们设想把相关的数据和操作放在一起,形成一个整体,与外界相对分隔。这就是面向对象的程序设计中的对象
8.面向过程的结构化程序设计:程序=算法+数据结构
9.面向对象的程序组成:
程序 = 算法 + 数据结构
程序=(对象+对象+对象+……)+ 消息
消息的作用就是对对象的控制
10.程序设计的关键是设计好每一个对象以及确定向这些对象发出的命令,使各对象完成相应的操作。
11.
1.对象的定义
1.类的公共代码只分配一次空间,对象只是对各自数据分配空间,公共代码部分只是重用类的代码;在建立对象时,只为对象分配用于保存数据成员的内存空间,而成员函数的代码为该类的每一个对象所共享
2.成员函数的内外定义,都不占对象的存储空间;3.定义的方式:
方式一
class Location
{
private:
int x,y;
public:
int set(int a,int b)
{
return a+b;
}
}dot1,dot2; //定义了两个对象
方式二:
类名 标识符,标识符2
Location dot1, dot2;
4.一个对象必定属于某个类(判断:错误)
2.对象成员访问:
(1)通过对象调用成员
(2)通过指向对象的指针访问成员
(3)通过对象的引用调用成员
class Myclock
{
private:
int hour, minute,second;
public:
void init();
void update();
void display();
};
//定义了对象clock 和 Myclock类指针pclock
Myclock clock,*pclock
clock.init();
// pclock 指向对象 clock
pclock = &clock
pclock->display();
//引用调用
Myclock &t2=clock
t2.display();
clock.hour = 4; //这里错误的,不能访问私有数据
3.new与对象数组
1.用new运算符来动态生成对象数组时,自动调用构造函数,而用delete运算符来释放p1所指向的对象数组占用的存储空间时,在指针变量的前面必须加上[ ], 才能将数组元素所占用的空间全部释放。否则,只释放第0个元素所占用的空间
pa1=new A[3];
.....
delete [ ]pa1;
delete pa1;只是释放了p[0]元素
3.假定AB为一个类,该类中含有一个指向动态数组空间的指针成员pa,
则在该类的析构函数中应该包含有一条__________语句。
答案:delete []pa
this指针
1.因为类的函数是公共代码,放在一处存储空间,但是对象的数据是不同存储空间,所以函数他怎么知道哪个对象使用它?因为this指针,this指针指向谁,就是谁调用函数代码;
在 C++ 中,每一个对象都能通过 this 指针来访问自己的起始地址。
2.this指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象,可以说this指针近似于只有对象才能使用,超出对象的都不是,如静态数据。
3. 通俗的讲:this指针就像对象的钥匙,有它才可以访问对象内部数据,没它就不能直接访问非静态的数据成员(详细看静态成员函数部分)4.友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针
#include <iostream>
using namespace std;
class Box
{
public:
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
}
double Volume()//体积
{
return length * breadth * height;
}
int compare(Box box)
{
return this->Volume() > box.Volume(); //这里就相当于 Box1.Volume()
}
private:
double length;
double breadth;
double height;
};
int main(void)
{
Box Box1(3.3, 1.2, 1.5);
Box Box2(8.5, 6.0, 2.0);
if(Box1.compare(Box2)) //这里调用了对象Box1 的compare成员函数 而在它函数内其实可以用this指针来访问
{
cout << "Box2 is smaller than Box1" <<endl;
}
else
{
cout << "Box2 is equal to or larger than Box1" <<endl;
}
return 0;
}
5.访问本对象的成员:(必须是在函数中用)
1.this->name;
2.(*this).name;//*this 相当于当前对象
6.this指针应用
Student::copy(Student &a){
if(&a==this)当需要判断对象是否是本身对象时
cout<<"啥都不做";
}
7.this指针具有如下形式的缺省说明:
类名 *const this;
即 this 指针无法改变指向
8.当对一个对象调用成员函数时,编译程序先将对象的地址赋给this指针,然后调用成员函数
练习
1.关于this指针的说法正确的是(c)。
(a) this指针必须显式说明
(b) 定义一个类后,this指针就指向该类
(c) 成员函数拥有this指针
(d) 静态成员函数拥有this指针
2.若采用x.abc(y)表达式调用一个成员函数,在成员函数中
使用的*this就代表了类外的x对象。(因为this指向该对象地址)
3.若采用p->abc(y)表达式调用一个成员函数,在成员函数中
使用的_this__就代表了类外的p指针。
4.以下关于this指针的描述错误的是( D )。
A所有的类都一定包含这个成员
B这个成员的赋值与销毁是由系统完成的
C它指向的是对象的地址
D它指向的是类的地址
5.以下关于this指针的描述正确的是( B )。
A this指针指向的是1个类
B this指针指向的是1个类对象
C this指针必须在类中显式的实现后才能使用
D 可以通过类的对象名来调用this指针
构造函数
特征
- 他们都没有返回值说明,也就是说定义构造函数和析构函数时不能指出函数的返回值的类型,即使void 也不能有
- 他们不能被继承
- 析构函数可以有默认参数
- 析构函数可以是虚的,但构造函数不可以是虚的
- 不可取他们的地址
- 不能常规调用方法调用构造函数,当使用完全的限定名时可以调用析构函数,比较特殊是,在对象数组中,初始化数组元素时可以显试调用析构函数
1)构造函数的定义
(1) 类名::同类名(参数表)
X::X(参数)正确
int X::X(参数)非法
(2)构造函数名跟类名一样
(3)一个类可以有多个构造函数
(4)构造函数允许,没有值的return
(5)一般用来初始化对象
(6)在生成对象的时候编译器自动生成构造函数
(7) 若定义的类要说明该类的对象时,构造函数必须是公有的成员函数。如果定义的类仅用于派生其它类时,则可将构造函数定义为保护的成员函数。
#include<iostream>
using namespace std;
class queue
{
int sloc,rloc; //sloc 队尾, rloc 对首
int q[100];
public:
queue();
void qput(int i);
int qget();
};
queue::queue() //构造函数
{
sloc=rloc=0;
return;
}
void queue::qput(int i) //i 为传入数
{
if(sloc==99)
{
cout<<"队列满了"<<endl;
return;
}
sloc++;
cout<<sloc<<endl; //这条语句证明 sloc++ 并不是一个劲加到 4 而是根据两个对象中的sloc 自增的
q[sloc]=i;
}
int queue::qget()
{
if(sloc==rloc) //证明队列空的
{
cout<<"队列为空"<<endl;
return 0;
}
rloc++;
return q[rloc];
}
int main()
{
queue a,b;
a.qput(10);
b.qput(20);
a.qput(20);
b.qput(19);
cout<<a.qget()<<endl;
cout<<a.qget()<<endl;
cout<<b.qget()<<endl;
cout<<b.qget()<<endl;
}
结果为:
1
1
2
2
10
20
20
19
2)构造函数对数据成员初始化
1.普通构造函数初始化:不带参,所有都由构造函数初始化
(1)在构造函数的函数体中进行初始化,例如:
类外初始化:(也有类内初始化,这里不演示了)
Circle::Circle(float r)
{
radius = r;
}
2.带参数的构造函数初始化:带参,动态初始化
class Student
{
public:
Student(int a,int b):num1(a),num2(b)
{
cout<<"初始化"<<endl;
}
private:
int num1;
int num2;
}
3.用参数初始化表初始化:带参,动态初始化
常量和引用的初始化用这种初始化方式;类的常成员函数和引用成员必须在构造函数的初始化列表中进行初始化
Circle::Circle(float r)::radius(r)
class A
{
private:
const int ten;
int&ref ;//引用
public:
A(int&i)::ten(10),ref(i)
}
3)构造函数的重载
#include<iostream>
using namespace std;
class test
{
private:
int a,b;
public:
test();
test(int c,int d);
} ;
test::test()
{
cout<<"这是第一个构造函数"<<endl;
return;
}
test::test(int c,int d)
{
cout<<"这是第二个构造函数"<<endl;
cout<<c+d;
return;
}
int main()
{
test a;
test b(13,14);//这里注意下
}
结果:
这是第一个构造函数
这是第二个构造函数
27
4)默认构造函数
- 当类中没有定义构造函数,系统默认自动创建一个无参数的构造函数,他负责创建对象,不负责初始化,其新产生对象的数据成员的值是不确定的
- 只要类定义了一个构造函数(不管有没有参数),系统将不再提供默认构造函数
- 如果已经有构造函数了想用没有有参数的构造函数,则必须自己定义
#include<iostream>
#include<string>
using namespace std;
class Student
{
public:
Student(char*Pname)
{
cout<<"call one "<<endl;
strncpy(name,Pname,sizeof(name));
//strcpy(s1,s2,n);作用是将字符串s2 复制到s1 中,最多复制 n 个字符
}
name[sizeof(name)-1]='\0';
cout<<"the name is "<<name<<endl;
Student()
{
cout<<"call no"<<endl;
}
protected:
char name[20];
};
int main()
{
Student noName; //调用了无参的构造函数
Student ss("jenny");//调用了有参
}
结果:
call no
call one
jenny
5)默认参数的构造函数
1.只用在声明构造函数时候默认参数,定义时则不行
2.带默认参数的构造函数也是默认构造函数
class Student
{
public:
//默认参数构造函数
Student(int a=10,int b=20):num1(a),num2(b)
{
cout<<"初始化"<<endl;
}
private:
int num1;
int num2;
}
3.如果构造函数定义了全部默认参数的构造函数,不能再定重载构造函数
Box();
Box(int,int);
BOx(int a=1,int b=2,int c=3);
Box a;非法,有歧义
Box b(15,16);非法,有歧义
4.在类中,若定义了没有参数的构造函数,或各参数均有缺省值的构造函数也称为缺省的构造函数,缺省的构造函数只能有一个。???
5. 产生对象时,系统必定要调用构造函数。所以任一对象的构造函数必须唯一
6)new与构造函数
1.可以使用new运算符来动态地建立对象。建立时要自动调用构造函数,以便完成初始化对象的数据成员。最后返回这个动态对象的起始地址
2.用new运算符产生的动态对象,在不再使用这种对象时,必须用delete运算符来释放对象所占用的存储空间
3.用new建立类的对象时,可以使用参数初始化动态空间。
4.要释放对象的数据成员用new运算符分配的动态空间时,必须显式地定义析构函数
7)不同类型对象的构造和析构调用顺序
析构函数
1.执行析构函数的四种情况
①函数中的对象,函数结束时,调用析构函数
②静态局部对象在函数调用结束时不释放而在main结束后
③全局对象,在main结束,或exit函数调用后
④如果用new动态建立一个对象,先调用该对象的析构函数,再在析构函数中用delete来释放对象;因为在撤消对象时,系统自动收回为对象所分配的存储空间,而不能自动收回由new分配的动态存储空间。
2.析构函数不返回任何值,也没有类型
3.不能被重载,一个类只有一个
4.析构函数本质是,结束释放对象前,需要执行什么操作
5.用户没有定义,则编译系统自动生产一个
6.执行顺序同,入栈和出栈
7.定义格式
class Student
{
public:
~Student(){
cout<<"end"<<endl;
}
}
8.析构函数没有参数
对象数组
对象数组:指该数组的所有元素都是对象
#include<iostream>
using namespace std;
class test
{
private:
int num;
float f1;
public:
test();
test(int n , float f);
int getint()
{
return num;
}
float getfloat()
{
return f1;
}
};
test::test()//无参构造函数
{
cout<<"Initializing default"<<endl;
num = 0;
f1=0.0;
}
test::test(int n,float f)//有参
{
cout<<"Initializing"<<" "<<n<<","<<f<<endl;
num=n;
f1=f;
}
int main()
{
cout<<"the main function:"<<endl;
test array[2];//产生2个对象都是无参数
test s(1,3.14);
cout<<"the second element of array is "<<array[1].getint()<<endl;
}
结果:
the main function:
Initializing default
Initializing default
Initializing 1,3.14
"the second element of array is 0
对象数组2:
#include<iostream>
using namespace std;
class test
{
private:
int num;
float f1;
public:
test(int n);
test(int n , float f);
};
test::test(int n)
{
cout<<"Initializing "<<n<<endl;
num = 0;
}
test::test(int n,float f)//有参
{
cout<<"Initializing"<<" "<<n<<","<<f<<endl;
num=n;
f1=f;
}
int main()
{
test array1[3]={1,2,3};//对应的是一个参数的构造函数
test array2[]={test(2,3.5),test{4}};//对应的是两个参数的构造函数 和一个参数的构造函数
test array3[]={test(5.5,6.5),7.5,8.5};//创建了三个对象
}
对象指针
1.对象的空间起始地址就是对象的指针
2.定义格式
Student *p;
Student a;
p=&a;
1)对象指针访问成员方式
1.*p.ID;//调用方式①
2.p->getname();//方式②
2)指向对象成员的指针变量
int *p;
p=&a.ID;
3)指向对象成员函数的指针
1.与指针函数不同,它多了一项,即需要指针与指向的对象函数是属于同个类的,而指针函数只是普通指针,没有类,但指向对象成员函数的指针是有类的
2.定义格式
void (Student::*p)())
数据类型 (类名::*指针变量名)());
p=&Student::getname;//赋值类的成员函数首地址给指针
公用数据的保护
1)常对象
1.定义格式
Student const t1;
2.常对象,对象中的数据成员的值都不能被修改
3.常对象只能调用常成员函数,无法调用普通函数
class Student{
public:
void test1();//普通函数
void test2 const();//常成员函数
};
Student a;
a.test1();非法
a.test2();正确
4.常成员函数可以使用对象数据成员,但是仍然无法修改;
5.如果硬要加部分可以修改的数据成员,则数据类型声明mutable
6.用法类似const常变量,只是名字改成对象,所以它只能被常对象指针所指向,不能是普通的对象的指针
2)常对象成员
常数据成员
1.只能通过构造函数的参数初始化表对常数据成员进行初始化,任何其他函数都不能对数据成员赋值
class Student{
public:
const char name;
Student();
};
Student::Student(char h="z"):name(h);
常成员函数
1.定义格式
void test2 const();//常成员函数
2.应用:课本269页
3)指向对象的常指针:A const*p
助记:
1.const *p 叫常指针(可以看到直接顺着读就行了),
2.同理const A*p 叫常对象指针(A是类,自然含义是对象)
而const紧跟的部分是没法改变的,如1:const后面紧跟的是指针
所以该指针无法改变指向,如2:后面紧跟的是A即是对象,所以
该对象的内容没法改变
1.对象型常指针,不可改变指向的对象
2.但可以改变指向对象的数据成员
Studnet a,b;
Student *const p;
p=&a;
p=&b;非法
p->name="更改" 合法
4)指向常对象的指针变量:const A *p 可以改方向
1.用法与指向常变量的指针一样
2.不可以通过指针改变对象成员
3.可以改变指针指向的对象
4.可以通过对象直接修改成员数据
Student a;
const Student *p=&a;//指向常对象的指针
p->name="haha";非法,不可以通过指针改变对象成员
a.name="haha";合法,可以通过对象直接修改成员数据
5.常被用于做形参,目的是保护对象在函数使用时不被修改
void fun(const Student *p){
p->name="哈哈";非法,防止被修改
}
int main(){
Student a;
fun(&a);
}
5)对象常引用
1.&表面是说对变量起别名,实际就是一个常指针即const*p
2.常做函数的形参
const Student &a //常引用
相当于
const Studnet *p
(正确)
对象的动态建立和释放
1.为了使得对象被使用的时候才创建,所以引入了new
2.使用格式:
Box *p;类指针
p=new Box;
Box *p2=new Box(12,11);
3.new后,开辟了一段内存空间,并生产并存放一个Box对象,并返回指向这个对象地址的指针
4.空间满或没法new新空间,则会返回0指针值5.delete释放指针指向的内存空间:delete p;
对象的赋值
1)赋值
1.使用格式:
Student a,b;
a=b;对象赋值
2.实际上就是对象的数据成员一一赋值给另一个对象数据成员,必须是同个类
3.只对数据成员赋值,不对成员函数赋值,因为对象存储空间只是存放值数据,没有代码数据
4. 类的数据成员不能包括动态分配的数据,否则赋值时可能出现严重后果
拷贝构造函数
定义:是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象
Studnet(const Student &r)//const一般被省略不写
类(对象常引用)
一、需要调用拷贝构造函数的情况:
1)对象以值传递的方式传入函数参数:
class Example
{
private:
int a;
public: //构造函数
Example(int b)
{
a = b;
cout<<"creat: "<<a<<endl;
}
//拷贝构造
Example( Example& C)
{
a = C.a;
cout<<"copy"<<endl;
}
//析构函数
~Example()
{
cout<< "delete: "<<a<<endl;
}
void Show ()
{
cout<<a<<endl;
}
};
//全局函数,传入的是对象
void Fun(Example C)
{
cout<<"test"<<endl;
}
int main()
{
Example test(1);//产生对象
Fun(test);//传入对象
//test对象与 C对象
return 0;
}
调用Fun()时,会产生以下几个重要步骤:
(1) test对象传入形参时,会先会产生一个临时变量,就叫 C 吧。
(2).然后调用拷贝构造函数把test的值给C。 这两个步骤有点像该操作:Example C(test);
(3).等Fun()执行完后, 析构掉 C 对象。
2)对象以值传递的方式从函数返回
cat f()//定义函数返回类型为对象
{
cat a;
.......
return a;
}
cat b;
b= f() //系统自动调用拷贝构造函数
3)用一个对象去初始化另一个对象
(1)Student a;
Student a(b);用b对象复制出a对象
(2)Studnet a=b;用b对象初始化a对象,与对象赋值不同,这是初始化
对象赋值和对象复制的区别:
对象赋值时对一个已经存在的对象赋值,因此必须先定义被复制的对象,才能进行复制
对象复制则是从无到有的建立一个新对象,并使他与一个已有的对象完全相同
二、深拷贝和浅拷贝
1.如果没有定义完成拷贝功能的构造函数,编译器自动生成一个隐含的完成拷贝功能的构造函数,依次完成类中对应数据成员的拷贝。
2.但是,当类中的数据成员中使用new运算符,动态地申请存储空间进行赋初值时,必须在类中显式地定义一个完成拷贝功能的构造函数,以便正确实现数据成员的复制。
下面为没有显示调用而产生的报错情况:
(对上图补充:拷贝构造函数也是构造函数,所以执行了粉色部分构造函数就不会再执行上面蓝色部分的构造函数)
即要手动在拷贝构造函数中new空间再赋值,而不是简单的赋值
1)默认拷贝构造函数
#include<iostream>
using namespace std;
class Rect
{
public:
Rect() // 构造函数,计数器加1
{
count++;
}
~Rect() // 析构函数,计数器减1
{
count--;
}
static int getCount() // 返回计数器的值
{
return count;
}
private:
int width;
int height;
static int count; // 静态成员做为计数器
};
int Rect::count = 0; // 初始化计数器
int main()
{
Rect rect1;
cout<<"The count of Rect: "<<Rect::getCount()<<endl;
Rect rect2(rect1);
/* 使用rect2复制rect1
但输出的静态值还是1,这是因为他执行的是拷贝构造函数,
而不是上面写的构造函数,自然就不会有cout++语句了*/
cout<<"The count of Rect: "<<Rect::getCount()<<endl;
return 0;
}
这段代码对前面的类,加入了一个静态成员,目的是进行计数。在主函数中,首先创建对象rect1,输出此时的对象个数,然后使用rect1复制出对象rect2,再输出此时的对象个数,此时有两个对象存在,但实际程序运行时,第二个输出语句输出的是 1。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。
说白了,就是默认拷贝构造函数只负责复制,自然没有处理静态数据成员的自增语句cout++。
所以计数器没有递增,我们重新编写拷贝构造函数,
加
Rect(const Rect& r) // 拷贝构造函数
{
width = r.width;
height = r.height;
count++; // 计数器加1
}
就可以解决问题
2)浅拷贝(上面已有,略看)
所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让我们考虑如下代码:
class Rect
{
public:
Rect() // 构造函数,p指向堆中分配的一空间
{
p = new int(100);
}
~Rect() // 析构函数,释放动态分配的空间
{
if(p != NULL)
{
delete p;
}
}
private:
int width;
int height;
int *p; // 一指针成员
};
int main()
{
Rect rect1;
Rect rect2(rect1); // 复制对象
return 0;
}
在这段代码运行结束之前,会出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作。我们来分析一下:
在运行定义rect1对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况大致如下:
在使用rect1复制rect2时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时 rect1.p = rect2.p,也即这两个指针指向了堆里的同一个空间,如下图所示:
当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。
3)深拷贝(上面已有,略看)
class Rect
{
public:
Rect() // 构造函数,p指向堆中分配的一空间
{
p = new int(100);
}
Rect(const Rect& r)
{
width = r.width;
height = r.height;
p = new int; //new 为新对象重新动态分配空间:新空间
*p = *(r.p); //让这个新指针指向地址的值 为 旧指针p 的值:100
}
~Rect() // 析构函数,释放动态分配的空间
{
if(p != NULL)
{
delete p;
}
}
private:
int width;
int height;
int *p; // 一指针成员
};
类的聚集—— 含对象成员
-
类的聚集,是指一个类 含有另一个类的对象
-
可以实现已有的抽象上更加抽象
-
类聚集的构造函数设计:
原则:不仅要为本类中的基本类型成员数据赋初值,也要对对象成员初始化:
类名::类名(本类参数):对象1(参数),对象2(参数) {本类构造函数体};
例一
#include<iostream>
#include<string.h>
using namespace std;
class student
{
public:
student()
{
coutt<<",学生来了"<<endl;
}
~student()
{
cout<<"学生走了"<<endl;
}
} ;
class teacher
{
public:
teacher()
{
cout<<"老师来了"<<endl;
}
~teacher()
{
cout<<"老师走了"<<endl;
}
} ;
class meeting
{
public:
meeting()//:t(),s() 这里加不加都可以因为对象成员没参数不用初始化
{
cout<<"会议开始"<<endl;
}
~meeting()
{
cout<<"会议结束"<<endl;
}
protected:
teacher t;
student s;
} ;
int main()
{
meeting m;
}
结果:
学生来了
老师来了
会议开始
会议结束
老师走了
学生走了
例二:
#include<iostream>
#include<string.h>
using namespace std;
class StudentID
{
public:
StudentID(int ac=0)
{
value = ac;
cout<<"Assigning student ac:"<<value<<endl;
}
~StudentID()
{
cout<<"删除ac :"<<value<<endl;
}
private:
int value;
};
class Student
{
public:
Student(char*pName,int ssid=0):id(ssid)
{
cout<<"创建学生名字:"<<pName<<endl;
strncpy(name,pName,sizeof(name));
name[sizeof(name)-1]='\0';
}
~Student()
{
cout<<"删除学生"<<name<<endl;
}protected:
char name[20];
StudentID id; // 这是对象成员
};
int main()
{
Student s(" wang",9901);
//Student t("li");
}
- 首先创建对象 s 所以执行 聚类Student 的构造函数
- 因为发现有对象成员id(ssid),所以执行对象的构造函数StudentID
- 执行完再执行聚类的构造函数
- 再执行聚类的析构函数
- 再执行对象成员的析构函数
结果
静态成员
1)静态数据成员
1.静态数据成员被存储在内存的某一单元内,该类的所有对象都可访问它,无论创建多少个该类的对象,都只有一个静态数据副本。
2.即使没有创建任何一个该类对象,类的静态成员在存储空间中也存在,可以通过类名的解析运算符::直接访问。或用对象间接访问
Student::age;
Student a;
a.age;
3.含有静态数据成员在创建对象的时候不为它分配空间(因为在最初类外初始化时候已经分配一个空间了)
4.适用于:所有对象共享时,可以设定为静态成员
5.声明了类但是未生成对象,则类一般数据成员一般不占空间,只有定义对象时,才为对象的数据成员分配空间。
6.静态数据成员只能在类体外初始化
int Student ::age=10;
7.不能用构造函数参数初始化表对静态数据成员初始化
Student(int a)::age(a){
……
}
8.引用静态成员
类名::变量名
例:
#include<iostream>
#include<string.h>
using namespace std;
class A
{
static int i;
public:
A()
{
i++;
cout<<i<<endl;
}
};
int A::i=0;
int main()
{
A a1,a2,a3;
}
结果:1 2 3
9.静态成员只是 在初始化限制,在用的时候是可以换值的
class A{
static int temp;
A(int a=30;){temp=a;}
};
A::temp=1;
int main(){
A a(10);
A b(20);
}
10.在编译时,就要为类的静态数据成员分配存储空间
11.静态变量缺省的初值为0,即没有手动初始化时默认为0,所以静态数据成员总有唯一的初值
12.静态数据成员与全局变量一样都是静态分配存储空间的,但全局变量在程序中的任何位置都可以访问它,而静态数据成员受到访问权限的约束。必须是public权限时,才可能在类外进行访问。
2)静态成员函数
- 静态成员函数无this指针,所以不能直接访问非静态的数据(通俗讲,静态成员函数是存在一个公用空间,当然只能访问静态数据也就是公用数据,不可以访问不是公共的数据,不然他就失去了公共性);其必须用对象来访问;而非静态成员函数则可以访问静态和非静态数据成员;
例1
class A
{
int member;
static int cxk;
public:
static void fun(int i,int j, A obj);//对象
};
int A::i=0;
void A::fun(int i,int j, A obj)//静态函数
{
member = i;//因为 member变量不是静态所以这里会出错
cxk = j; //正确
obj.member= i ;//正确
}
2.静态成员函数直接用了非静态数据是不可以的
class A
{
static int total_length;//静态
int length;
public:
static int getdata()
{
total_length += length; //注意这里,因为无this指针
return total_length;
}
} ;
要点:
因为静态成员函数没有this指针,所以创建对象时候它并不知道到这个length是哪个对象的数据
所以可以改成:
static int getdata(A obj) //意味着形参是个对象,所以到时候会调用拷贝构造函数,如果我们的类中有动态new,则自己创建拷贝构造函数
{
total_length += obj.length; //注意这里
return total_length;
}
或
static int getdata(A &obj)
{
total_length += obj.length; //注意这里
return total_length;
}
- 在静态成员函数中访问的基本上是静态数据成员或全局变量
- 由于静态成员函数属于类独占的成员函数,因此访问静态成员函数的消息接收者不是类对象,而是类自身。调用静态成员函数的前面,必须缀上对象名或类名。
- 一个类的静态成员函数与非静态成员函数不同,调用静态成员函数时无需向它传递this指针,它不需要创建任何该类的对象就可以被调用。静态成员函数的使用虽然不针对某个特定对象,但使用时系统中最好已经存储该类的对象,否则无意义
- 静态成员函数不能是虚函数,因为静态成员函数也是在编译时分配存储空间,所以在程序的执行过程中不能提供多态性。
;若非静态成员函数和静态成员函数具有相同的名字和参数类型将是非法的。
#include<iostream>
#include<string.h>
using namespace std;
class student
{
public:
student(char*pName="no name")
{
cout<<"creat one student"<<endl;
strncpy(name,pName,40);
name[39]='\0';
count++;
cout<<count<<endl;
}
~student()
{
cout<<"删除学生"<<endl;
count--;
cout<<count<<endl;
}
static int number()//静态成员函数
{
return count;
}
protected:
static int count; //静态成员数据
char name[40];
};
int student::count=0; //初始化静态成员数据
void fun()
{
student s1; //创建一对象,count++
student s2;
cout<<student::number()<<endl;//调用静态成员函数需要类名
}
int main()
{
fun();
cout<<student::number()<<endl;
}
- 静态成员是在编译时候就完成确定了;即.对static静态局部变量时在编译时赋初值的,即只赋值一次,在程序运行期间,不再赋值,即值不再变
#include<iostream>
using namespace std;
class A{
public:
A();
void display();
private:
int i;
static int k;
};
A::A(){i=0;k++;}
void A::display(){cout<<"i="<<i<<",k="<<k<<endl;}
int A::k=0;
int main(){
A a,b;
a.display();
b.display();
return 0;
}
7.静态成员函数不能被派生类继承
8.静态成员函数的实现部分在类定义之外定义时,其前面不能加修饰词static。这是由于关键字static不是数据类型的组成部分,因此,在类外定义静态成员函数的实现部分时,不能使用这个关键字
(补充)
指向静态成员函数,可以省掉第一个类名**
友元
1)友元函数
- 友元函数是在类中说明的函数,在类外定义,但实则可以把它当做全局函数,且可以访问类中所有成员
- 是类的一个窗口,对类的信息封闭打开了一个窗口
- 友元函数相当于类的成员函数的功能,但本身它不是类的成员函数所以没有this指针,不能直接用类的数据,故需要参数传进一个对象,进而用对象改变数据,不能直接访问
a.age=10;非法
void display(Student &a){
a.age=10;正确
age=10;非法
}
- 类外定义可以去掉friend
- friend 类型 函数名(对象参数)
- 友元函数不是本类的函数但是它可以用其私有成员
class Student{
public:
friend void display(Student &a);定义友元类
private:
int age;
}
void display(Student &a){
a.age=10;//按理来说是无法访问类的私有成员的,但
//它是Student的友元函数,所以相当于类的成员函数,则可以访问
}
int main(){
Student a;
display(a);
}
7.一般用友元会用到类的提前引用;
class B;提前引用
class A{
A(B&a);用到了B类
};
class B{};
8.普通函数是无法用类的提前引用的
class A;
void display(A&a){};非法
class A{};
class A{};
void display(A&a){};正确
9.友元函数与一般函数的区别
①友元函数必须在类的定义中说明,其函数体可在类内定义,也可在类外定义;
②它可以访问该类中的所有成员(公有的、私有的和保护的),而一般函数只能访问类中的公有成员。
10.友元函数不受类中访问权限关键字的限制,可以把它放在类的私有部分,放在类的公有部分或放在类的保护部分,其作用都是一样的。换言之,在类中对友元函数指定访问权限是不起作用的
class A{
public:
friend void test();//放公有
private:
friend void test();//放私有
完全是一样的
}
11.谨慎使用友元函数
通常使用友元函数来取对象中的数据成员值,而不修改对象中的成员值,则肯定是安全的。
2)友元成员
- A类的成员函数声明为,B的友元函数,则可以通过A的成员函数访问 B类的数据,实现了类与类的互通
Class A{
private:
int x;
public:
friend B::test(A& a);
};
Class B{
void test(A &a){
cout<<a.x; 正确,虽x成员是a对象的私有成员,按理来说是
无法直接访问私有变量的,但是呢test函数是A类的
友元函数,所以呢,可以直接访问私有变量
}
}
int main(){
A a;
a.x 非法,这样就无法直接访问了
}
#include<iostream>
#include<string.h>
using namespace std;
class student;
class grate//必须grate类先全部写出来,因为student中友元函数是根据grate的成员函数来的,如果student先写会报错
{
private:
int English;
int Chinese;
public:
grate(int i,int j)
{
English=i;
Chinese=j;
cout<<"英语成绩:"<<English<<endl;
cout<<"语文成绩: "<<Chinese<<endl;
}
void show(student&st); //要让 grate 类具备输出名字的能力,定义一个成员函数
};
class student
{
friend void grate::show(student&st) ; //将grate成员函数定位类student 的友元函数
char name[10];
public:
student(char*s1)
{
strcpy(name,s1);
}
};
//类外定义 成员函数
void grate::show(student&st)
{
cout<<"name:"<<st.name<<endl;
}
int main()
{
student A("wang");
grate B(78,99);
B.show(A);
}
2.为了防止出现两个类相互套用,即鸡生蛋蛋生鸡的问题,采用提前声明类
class A;
class B;
3.这样的提前声明是没法创建对象的,只有定义了才可以
class A;
class B;
A a;非法
class A{};
A a;正确
3)友元类
1.A是B的友元类,则A可以访问B类的所有成员
2.是单向的不是双向
3.友元性质不具备传递性,也不具备继承性
#include<iostream>
using namespace std;
class node
{
int data;
node*next;//对象指针
public:
node(int i,node*p) //产生每个结点的数据
{
data=i;
next=p;
}
friend class stack;//友元类
};
class stack
{
node*top;//因为友元类所以可以调用
node*n;
public:
stack()
{
top=0;//初始化top头指针
}
void push(int i);//入栈
int pop();//出栈
} ;
void stack::push(int i)
{
node*n=new node(i,top);//这里的top是指向上个结点的 本身,现在创建新节点给了新节点的next指针
top=n;//新节点就给了top指向;
// top节点一直指最新节点,当要插入新节点时才需要更换top指向
//top 节点只有一个 ,是 s对象的成员数据
}
int stack::pop()
{
int d;
node*t = top;//记录这个节点,因为后面要释放
if(top)
{
d=t->data;
top=top->next;
delete t;//释放节点
return d;
}
return 0;
}
int main()
{
stack s;
int i=0,d;
cout<<"循环输入数进栈:"<<endl;
while(i<4)
{
i++;
cin>>d;
s.push(d);
}
cout<<"链栈满了"<<endl<<"顺序输出栈"<<endl;;
while(i>0)
{
i--;
cout<<s.pop();
}
}
4.友元类的应用:
不管是按哪一种方式派生,基类的私有成员在派生类中都是不可见的。如果在一个派生类中要访问基类中的私有成员,可以将这个派生类声明为基类的友元
class Base {
friend class Derive;
.....
}
class Derive {
.....
}
类模板
类似函数模板
1.功能相同,仅仅数据不同
2.定义:
template<class numtype>// 类型名 numtype
class Compare{类模板名
public:
Compare(numtype a)构造函数
{x=a;}
numtype max(){ return 0;}
private:
numtype x;
}
用类模板生成个实际对象
Compare <int> a(14);
这样编译器就让 numtype 变成int类型
3.类外定义类模板中的成员函数
numtype Compare<numtype>::max(){retrun 0;}
4.一个普通基类能派生类模板
5.类模板从普通类派生,也可以类模板派生
6.根据建立对象时的实际数据类型,编译器把类模板实例化为模板类