一.类和对象的理解
1.定义:
- 类是对一组具有相同属性和行为的对象的抽象。
- 类是一种数据类型,对象是这种类型的变量。
- 类的基本思想是数据抽象和封装。
- 类要实现数据抽象和封装,需要先定义一个抽象数据类型(ADT)。
2.抽象数据类型(ADT):
- 抽象数据类型由两部分组成:一组数据和对这些数据的操作
3.类的写法:
class 类名
{
public:
公有成员(一般是成员函数,程序通过public成员函数可以操纵该类型的对象)
private:
私有成员 (只允许类自己的成员函数或友元访问,而类外的函数不能访问)
protected:
保护成员(可以由自己的成员函数,友元和派生类成员访问)
};
注:
C++通过限定成员的访问权限来设置边界,实现信息隐藏。关键字pubic、private和protected被称为访问限定符。访问限定符在类定义中使用。
访问限定符在类定义中的出现顺序和出现次数没有限制。一个访问限定符的作用会持续到出现下一个访问限定符或类定义结束。
4.类中成员函数
class Saledata
{
public:
double totalRevennue();
void read();
void print();
private:
string productNo;
double price=0.0;
unsigned unitSold=0;
};
void Saledata::read(){
cin>>productNo>>price>>unitSold;
}
void Saledata::print(){
cout<<productNo<<":"<<price<<" "<<unitSold<<" "<<totalRevennue()<<endl;
}
注:
- public部分一般是成员函数,private部分一般是数据成员。
- 成员函数在类外定义时,函数名字前面要加类名字和作用域符“ ::”,表示这个函数是在其所属的类作用域内,是这个类的成员函数,不同于全局函数。
- 成员函数的定义虽然处于类定义的花括号之外,但还是在类作用域内,所以可以自由访问类的成员,不需要成员访问语法。
二.this 指针
1. 每个对象都维护自已的一份数据,而成员函数定义是所有对象共享的,如何知道是对哪个对象的数据进行操作呢?这就引入了this指针。
- 每个成员函数都有一个隐含的参数,指向接收消息的对象,称为this指针, X类的this指针的类型是X*
- this指针是一个常量,含有当前实施调用的对象的地址。不能改变this指针的值,也不能取this指针的地址
2.用于理解this指针的含义
class X{
int m;
public:
void setVal(int v) { this->m=v;}
void inc(int d) { this->m+=d;}
void changeVal(int v) { this->setVal(v);}
};//在这段代码中this指针并不需要,只是为了帮助理解this指针的用法
3. this在成员函数中的用处:
(1)区分与局部变量重名的数据成员
(2)返回当前对象
(3)获取当前对象的地址
4.this指针的用法体现
class X{
int m;
public:
void setVal(int m){
this->m=m; ∥区分与函数参数重名的数据成员
}
X& add(const X& a){
m+=a.m;
return*this; //返回当前对象
]
Void copy(const X& a){ //复制对象
if(this ==&a)
return; //判断当前对象和a是否为同一对象,相同则无须复制
m=a.m; //复制操作
}
};
三.友元
1.对友元函数的理解
- 如果想让非成员函数访问一个类中的私有数据,应该在类中将这个函数声明为friend(友元)
- 友元函数可以直接访问类的私有成员。
- 它是定义在类外部的普通函数,不属于任何类。
- 需要在类的内部声明,声明时需加 friend 关键字
#include <iostream>
#include <cmath>
using namespace std;
//使用友元函数计算两点之间的距离
class Point{
public:
Point(int xx = 0, int yy = 0) { X = xx; Y = yy;}
int GetX() {return X;}
int GetY() {return Y;}
friend float fDist( Point &a, Point &b );
private:
int X, Y;
};
float fDist(Point &p1, Point &p2){
double x = double(p1.X - p2.X);//通过对象访问私有数据成员,而不是必须使用Getx()函数
double y = double(p1.Y - p2.Y);
return float(sqrt(x*x + y*y));
}
int main(){
Point p1(1, 1), p2(4, 5);
cout << "the distance is:";
cout << fDist(p1, p2) << endl;//计算两点之间的距离
return 0;
}
//可以看到在友元函数fDist()中通过对象名直接访问了Point类的私有数据成员X和Y。
2.对友元类的理解
友元必须在被访问的类中声明。一个类的友元可以是全局函数、另一个类的成员函数和一个类。类A是类B的友元隐含着A的所有成员函数都是B的友元。
友元关系是单向的,若定义类B是类A的友元,类A不一定是类B的友元。
友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元。
class A{
public:
int GetX() { return x; }
friend class B;//B类是A类的友元类
//其它成员略
private:
int x;
};
class B{
public:
void set(int i);
//其他成员略
private:
A a;
};
void B :: set(int i){
a.x = i;//由于B类是A类的友元类,所以在B的成员函数中可以访问A类对象的私有成员
}
四.构造函数和析构函数
1.构造函数:
构造函数是一种特殊的成员函数,能够在创建对象时被自动调用,负责对象的初始化。
构造函数的名字和类名字相同,它改有返回类型(注意:不是void类型)
构造函数的参数通常为数据成员提供初始值。
构造函数可以重载,在创建对象时,编泽器会根据初始值的类型和个数来调用相应的构造函数,因而构造函数的形式决定了初始化对象的方式。
2. 如果类的设计者没有写构造函数,那么编译器会自动生成一个没有参数的构造函数,虽然该无参构造函数什么都不做。如果编写了构造函数,那么编译器就不会自动生成默认构造闲数。
class Complex{
private:
double real, imag;
public:
Complex(double r, double i = 0); //第二个参数的默认值为0
};
Complex::Complex(double r,double i){
real = r;
imag = i;
}
那么以下语句有的能够编译通过,有的则不行:
Complex cl; //错,Complex 类没有无参构造函数(默认构造函数)
Complex* pc = new Complex; //错,Complex 类没有默认构造函数
Complex c2(2); //正确,相当于 Complex c2(2, 0)
Complex c3(2, 4), c4(3, 5); //正确
Complex* pc2 = new Complex(3, 4); //正确
3.构造函数重载
构造函数是可以重载的,即可以写多个构造函数,它们的参数表不同。当编译到能生成对象的语句时,编译器会根据这条语句所提供的参数信息决定该调用哪个构造函数。如果没有提供参数信息,编译器就认为应该调用无参构造函数。
class Complex{
private:
double real, imag;
public:
Complex(double r);
Complex(double r, double i);
Complex(Complex cl, Complex c2);
};
Complex::Complex(double r) //构造函数 1
{
real = r;
imag = 0;
}
Complex :: Complex(double r, double i) //构造数 2
{
real = r;
imag = i;
}
Complex :: Complex(Complex cl, Complex c2) //构造函数 3
{
real = cl.real + c2.real;
imag = cl.imag + c2.imag;
}
int main(){
Complex cl(3), c2(1,2), c3(cl,c2), c4 = 7;
return 0;
}
根据参数个数和类型要匹配的原则,c1、c2、c3、c4 分别用构造函数 1、构造函数 2、构造函数 3 和构造函数 4 进行初始化。初始化的结果是:c1.real = 3,c1.imag = 0 (不妨表示为 c1 = {3, 0}),c2 = {1, 2},c3 = {4, 2}, c4 = {7, 0}。
4.构造函数初始化列表
I. 初始化列表位于构造函数的参数表之后,函数体之前:
构造函数(参数表):初始化列表{函数体}
class Student
{
private:
int m_num,m_maths ;
public:
Student(){}//默认构造函数
Student(int num,int maths ):m_num(num),m_maths(maths)
{
cout<<m_num<<endl;
cout<<m_maths<<endl;
}
};
II. 如果成员是const,、引用,或者是未提供默认构造函数的类类型,就必须通过构造函数初始化列表为这些成员提供初值。
III. 在初始化列表中,每个成员只能出现一次。成员初始化的顺序与它们在类定义中出现的顺序一致。构造函数初始化列表中初始值的先后关系不会影响实际的初始化顺序。最好令构造函数初始化列表中的顺序与成员声明的顺序保持一致。
5.析构函数:
- 和类名一样,不过得在前面加上~
- 无参数,无返回值,所以不可以重载。
- 析构函数的名字是类名字前加波浪线“~”
6.委托构造函数
委托构造函数使用所属类的其他构造函数执行自己的初始化过程,把部分或全部职责委托给了其他构造函数
语法形式:
ClassName(参数表):ClassName(参数表){函数体}
class A
{
private:
int a;
int b;
char c;
char d;
public:
A(int num0,int num1,char C):a(num0),b(num1),c(C){}
A(int num0,char C):A(num0,0,C){}//b默认初始化为0
A(int num0):A(num0,'p'){b=1;}//b重新赋值为1
void getMembers()
{
cout<<a<<" "<<b<<" "<<c<<" "<<d<<endl;
}
};
在委托第二个构造函数构造时,b被初始化为0,这里我们在函数体内重新赋值为1,那么b到底是0还是1呢?结果是1。函数体内的初始化要晚于成员列表初始化,即委托其他构造函数构造完后,在进行函数体内的赋值
一个构造函数想要委托另一个构造函数,那么被委托的构造函数应该包含较大数量的参数,初始化较多的成员变量。而且在委托其他构造函数后,不能再进行成员列表初始化,而只能在函数体内进行初始化其他成员变量。
五.const用法
1.定义常量
可以为所有的内置类型对象使用const限定符,可以用const代替#define来定义常量
const int buf=100;
2.限定指针和引用
const限定指针的两种用法:(1)限定指针指向的对象 (2)限定指针中存放的内容
const修饰指针变量时:
(1)只有一个const,如果const位于*左侧,表示指针所指数据是常量,不能通过解引用修改该数据;指针本身是变量,可以指向其他的内存单元。
(2)只有一个const,如果const位于*右侧,表示指针本身是常量,不能指向其他内存地址;指针所指的数据可以通过解引用修改。
(3)两个const,*左右各一个,表示指针和指针所指数据都不能修改。
const int*pl; //P1是一个指向const int的指针,指向常量对象
int const*pl; //效果同上
int d=l;
int* const p2=&d; //P2是一个指向int的指针常量,一直指向d
const int* const p3=&d; //指针与其指向的对象都是常量
3.限定函数参数(const修饰符也可以修饰函数的传递参数)
void Fun(const int Var); //告诉编译器Var在函数体中的无法改变.
4.限定函数返回值(const修饰符也可以修饰函数的返回值,是返回值不可被改变)
const int Fun1(); const MyClass Fun2();
5.限定对象
限定的对象不能被修改,不能调用非成员函数。
6. 限定数据成员
限定的数据成员在创建对象时初始化,之后值不能修改。
7.限定成员函数
限定的成员函数不会修改数据成员的值,可以被const对象调用,也可以被非const对象调用。
定义const成员函数的语法形式为:
返回类型 成员函数名(参数表) const {函数体}
class X{
int m;
public:
X(int v=0):m(v){}
void set(int v){m=v;}
int get() const {return m;}
};
...
const X b(5); //const 对象
b.get(); //正确
b.srt(10); //错误
const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数。
六.static成员用法
1.static数据成员被当作该类类型的全局变量
对非static数据成员,每个对象都有自己的副本
static数据成员对整个类类型只有一个,由这个类型的所有对象共享访问
在类的数据成员声明前加关键字static,就使该数据成员成为静态的
2. 与全局变量相比,static数据成员的优点
I. static数据成员没有进入成员的全局作用域,只是在类作用域中,不会与全局域中的其他名字产生冲突
II. static成员仍然遵循访问控制规则 ,可以实现信息隐藏,static成员可以是private成员,而全局变量不能
3.static数据成员的初始化
static数据成员属于类,不属于某个特定对象,因而不能在构造函数中初始化
static数据成员在类定义之外初始化,使用类名字限定
int Object::count = 0;static成员只能定义一次
定义一般不放在头文件中,而是放在包含函数定义的源文件中static数据成员可以是任何类型,甚至是所属类的类型
4.static const数据成员
static const 数据成员在类外初始化
class Account {
static double rate;
double balance;
static const int maxClientNumber;
string clientName[maxClientNumber];
public:
…
};
const int Account::maxClientNumber = 2;
整值类型的static const可以在类定义里初始化
其他类型不能在类中初始化
class Account {
static double rate;
double balance;
static const int maxClientNumber = 2; //OK
string clientName[maxClientNumber];
static const string bankName = "BOC"; //错误
public:
…
};
5.static数据成员的访问
- 在类的成员函数中可以直接访问static数据成员
在非成员函数中通过两种方式访问static数据成员
成员访问运算符“.”或“->”
像访问普通数据成员的语法一样,通过对象或指针来访问类名限定的静态成员名
static成员只有一个副本,可以直接用类名字限定的静态成员名字访问 Class Name::StaticMemberName
static数据成员的访问
class Object {
static int count; //静态数据成员
…
friend void func(Object& obj);
};
void func(Object& obj){
cout << obj.count; //成员访问语法
cout << Object::count; //类名限定访问
}
6.static成员函数与普通成员函数
- 成员函数也可以访问static数据成员
- 普通成员函数必须通过类的对象或指针调用,而静态数据成员并不依赖对象存在
如果成员函数只访问静态数据成员,那么用哪个对象来调用这个成员函数都没有关系,因为调用的结果不会影响任何对象的非静态数据成员