关于C++的类详细解析(学习笔记)

类的定义

class 类名
{
  数据成员
  成员函数
};
//例如
class student{
//数据成员
public:
	char *name;
	int age;
	char sex;
//可以定义静态数据成员,也就是类数据对象,属于类而不是任意一个对象
private:
	static int type;
public:
//成员函数
	void set_age(int t_age)
	{
		age = t_age;
	}
	
};

这里的类名相当于一种数据类型,这种数据类型的变量叫做对象,专业的说法叫做实例化一个对象。
公共、私有和保护
private 只有该类的成员函数和友元函数才能访问这些成员
public 别的类也可以访问这些成员函数和数据对象
protected 在具有private特性的同时,可以由派生类(通过继承而生成的类)中的成员函数来访问。

数据成员和成员函数

成员函数的使用
首先定义一个类的对象,格式为:
类名 对象名
有了对象之后,就可以用对象来访问类中的成员函数了。可以用句点或者指针形式,定义一个指向对象的指针,数据类型为类名。将指针与对象关联:

class classname
{
  public:
    int x;
   protected:
     void function();
};
classname duixiang; //实例化一个对象
classname *p; //定义一个指向该类的指针
p=&duixiang;  //将对象赋值给指针,或者说让指针指向该对象。或者说将该对象的首地址赋值给该指针。
//当指针和对象关联之后,用指针访问数据成员和成员函数需要用->
//或者也可以用取内容运算符之后再用句点。
p->x = 0;#equal to duixiang.x;
//也可以这样: (*p).x = 0;
p->function;#equal to duixiang.function();
//也可以这样: (*p).function();

成员函数的内部定义和外部定义

定义在内部的函数一般比较简短,如果定义在外部,需要在类里面声明。将函数体写在外面,并且注意需要作用域限定运算符 ::左边是类名,右边是函数名。另外,一般情况都把类的定义写在头文件中。用的时候直接include该头文件即可。采用内部定义的函数默认为inline函数,如果希望外部函数也是inline函数,那么需要在内部,外部或者同时都加上inline关键字

问题:什么是inline函数???

解答:inline 函数又被称为内联函数或者内置函数,在编译的时候将函数代码直接插入到相应的位置,不再进行调用,相当于宏定义。采用inline函数能有效避免宏定义的副作用。在每个调用的位置上都有一个函数实体的拷贝。这样做是为了速度更快,但是如果函数本身很大,再每次调用的地方都插入,那么整个程序就很大,反而得不偿失。所以一般编译器都会最终决定你所声明的Inline函数是否适合直接插入到调用的位置。 另外,一些用户没有声明的小的函数也可能被编译器当作inline函数。

#宏定义带来的副作用
#define Max ((a)>(b)?(a):(b))
#调用时:
Max(x,y)
//但是如果是这样的情况:
((++x)>(++y)?(++x):(++y))
#x,y自加两次

inline函数调用时不需要额外的系统开销,用空间来换时间,只适用于规模较小函数。不是所有带有inline关键字的函数都可以作为inline处理,系统会根据函数大小决定,没有inline关键字的函数也可能被编译成inline函数。所以这只是一种请求,决定权在于编译器。
注意:inline函数体内不能有循环语句和switch语句。这里应该是运行的时候会循环展开,这样导致代码体积变大,违背了初衷。(循环展开是系统结构的知识,意思是将循环体展开一定的次数,找到能够流水线执行的程序,提高执行效率)

构造函数

不带参数的构造函数
定义一个类的对象时,如果不指定数据成员的初值,那么是不确定的。
而构造函数的作用就是在定义类的对象的同时为类中的数据成员赋初值。
1.构造函数的名字和类名相同
2.只用于设初值,因此没有返回值
3.生成类时自动调用
4.如果用户没有定义,系统将会自动生成一个空的什么也不做的构造函数。

// 如下所示,冒号后面是初始化列表,可以直接赋值的可以写在这里
classname::classname():x(99)
{
  //equal to x=99
}

带参数的构造函数
注意:如果定义了带参数的构造函数,那么在定义类的对象的同时需要在对象圆括号里面给出参数,而且需要几个参数给几个。如果定义了带参数的构造函数,没有定义不带参数的构造函数,系统也不会自动生成空的构造函数了。因为你已经有一个构造函数了。

classname::classname(int a,int b):x(a),y(b)
{
  //equeal to 
  //x=a;
  //y=b;
}
#定义类的对象时
classname duixiang(100,200); //初始化传入参数为100和200

构造函数的显式调用
一般都是自动调用,有需要时可以显示调用

classname duixiang = classname(100,200)//后面的classname是构造函数

构造函数的重载

重载:一个类中可以定义多个重名的构造函数
问题:函数重载的具体定义是什么?
解答:C++中,可以定义多个函数名相同的函数,只要函数的参数个数或者参数类型或者参数顺序不同即可,总之要有一定的区分度。
函数调用的时候,系统根据给定的参数类型或者个数自动选择。

注意:1.函数的返回值类型不能作为是否可以重载的判断条件,还未调用,编译器无法决定,返回值不具有参考性。
2.函数的重载和函数的默认参数一起使用时可能会引起函数重载的非法定义,编译器无法决定,最好不要在构造函数中使用默认参数。

void draw(int x=10,int y=20,int z=30)
void draw(int x,int y)
//非法的函数重载
//如下情况调用时无法决定选择哪个,造成冲突,报错
draw(10,20);

构造函数的调用

1.当需要调用只有一个参数的构造函数的时候有两种方法

//定义对象的同时隐式调用带有一个参数的构造函数
classname dt(10);
classname dt=10;

2.调用无参构造函数时不需要括号,带了括号就是函数了

classname dt;//定义对象的同时将会隐式地调用无参的构造函数
//可以定义返回类型是类的类型的函数
//前面说过类名相当于一种自定义的数据类型
classname function(){
  //返回数据类型为classname型
}

同理,可以设置函数参数类型为类的类型
函数的参数可以声明为类的类型或者其引用类型
(1)当函数的形参为类的变量时,形参的改变不影响其对应的实参的值
(2)当函数的形参为类的引用变量时,形参的改变将直接影响到对应实参的值。
理解:当函数的形参是类的引用变量时,实参传入的是地址,那么形参的改变导致这个地址的内容的改变,同时也就导致了实参的改变。

(补充说明)什么是引用??

引用是一个变量的别名,实际上底层的实现是指针,需要占用一定的内存空间。
引用变量中的值也是地址值,但是不能被改变,相当于一个常量指针。

#include<iostream>
using namespace std;

int main()
{
	int x = 0;  //定义一个int变量
	int &y = x; //定义一个int 引用,并将x的值赋值给他(实际上赋的也是地址)
	int *p = &x; // 定义一个int 指针,并将x的地址赋值给他
	cout<<"x = "<<x<<endl; // 0
	cout<<"y = "<<y<<endl; // 0 ,y和x的值一样,实际上是编译器自己加了*,输出的是*y,y的内容应该是x的地址。
	cout<<"p = "<<p<<endl; //0x6dfe84
	
	cout<<"x的地址为:"<<&x<<endl;  //0x6dfe84
	cout<<"y的地址为: "<<&y<<endl; //0x6dfe84 实际上这个地址是*y的地址,也就是x的地址,y的地址未知
	
	return 0;
}

变量的引用就是一个变量的别名,变量和变量的引用代表着同一个变量。
紧跟在数据类型后面的&符号就是引用的声明符号,其他情况都可以认为是取地址符号。引用在声明的时候就要初始化,除非是定义在函数的参数里面的时候不用初始化;否则会报如下错误:

[Error] 'b' declared as reference but not initialized

指针和引用的区别:
1.指针的值是可以改变的,改变之后指针变量指向别的目标变量。引用变量在定义时被指定了某一变量的地址之后就不能改变了。
2.指针既可以对值也可以对地址进行操作,引用只能对值操作,因为它的地址不能改变。

int x;
int &y;
int a;
y=&a;//错误,引用只能对值进行操作,而不能对地址操作
y=a;//正确
int *p=&a;//指针对地址操作
*p=x;//指针对值操作

所以说引用变量还是一个变量,可以对数值操作,但是有一个不变之处就是地址值不变。所以引用的内涵到底是什么呢??可以理解为:引用就像指针一样存着一个地址,但是这个地址是不能改变的,这个引用一直存着这个地址,而我们称它为变量是因为这个地址里面的内容可以改变

声明引用时,必须同时对其进行初始化。注意不能建立数组的引用。
这里可以这样理解:首先我们定义了一个变量 x,并初始化。然后定义一个相同类型的引用 y ,然后将该变量赋值给该引用 y = x。于是编译器为该引用变量y分配一个存储空间,用于存储所引用的变量的地址(&x),然后当我们输出引用的时候,编译器自动加上了取内容运算符,所以得到的就是值(*y),而不是地址。当我们对引用取地址的时候,取得是已经加了取内容运算符之后的东西( &(*y), 实际上是&x)的地址,所以得到的是相同得地址(因为 x == *y, 所以 &x == &(*y))。
将引用作为函数形参时,形参的改变将直接影响到实参的改变,因为此时直接操作的就是实参而不是实参的复制(对于值传递的函数来说是这样的)。

总结一下就是:引用结合了指针和变量,一个引用可以向他所指向的变量一样直接被赋值,也可以像指针一样,产生指针的作用,例如在swap函数中交换:

void swap(int& x, int& y){
	int temp = x; 
	x = y;
	y = temp;
	}

当为一个变量指定了引用之后,就可以直接通过这个引用操作,而不用变量自己抛头露面。

析构函数

~classname();//只有这么一种形式

析构函数是对象被释放时自动调用的成员函数。如果没有定义,系统会自动定义一个没有任何功能的析构函数。一般的类中都不需要析构函数。
注意:析构函数没有返回值,没有参数
当在对象中需要动态申请内存空间的时候,往往需要析构函数。
析构函数的作用就是进行后处理,如释放申请的空间等。

复制构造函数
先看一段代码

#include<iostream>
using namespace std;
class student{
	public:
		int age;
	public:
		student(){};
		student(int t_age){
			age = t_age;
		};
		void show_age();
};
void student::show_age()
{
	cout<<"age = "<<age<<endl;
}

int main()
{
	typedef student stu;
	stu s1(19),s2;
	s2 = s1;
	s1.show_age(); //19
	s2.show_age(); //19
	return 0;
}

注意:这里实例化了两个对象,s1给了参数,因此可以隐士调用只有一个参数的构造函数,而s2没有参数,因此需要调用默认构造函数,但是由于我们已经定义了一个带参数的构造函数,系统并不会自动生成默认构造函数,所以需要自己写一个,否则初始化s2的时候会报错。因此在定义了带参数的构造函数的时候最好保持一个良好的习惯:把默认构造函数也写上。

再看,这里s2没有赋值,我们用s1给他赋值,得到了一样的结果,说明对象之间可以直接赋值(这里是自动生成了默认复制构造函数,简单的将值copy)。但不是所有的情况都如此,例如,当成员变量是其他类的对象的时候,或者成员变量是指针的时候,简单的赋值只是将指针的地址赋值给另一个对象,那么两个对象操作的就是同一块存储空间,这样肯定不合适。

如果希望将已经存在的类对象的值赋给该类另一个对象,需要复制构造函数。
过程:通过赋值语句将作为 入口参数的类对象 中的数据成员的值赋给当前对象的对应数据成员中
意思就是将形参的类对象(一般是引用)赋值给当前对象(必须是同一个类的对象)
复制构造函数也是构造函数,所以函数名就是类名,没有返回类型,参数为对象引用
和一般构造函数的区别就是参数是对象引用

student::student(const student & another)
{
	this-> age = another.age; //通过赋值语句操作将形参的数据成员赋值给当前对象的对应数据成员。
}

**注意:**虽然传入的参数是对象的引用,前面说过,如果参数是对象的引用,那么形参的改变将直接影响到实参的改变。但是这里不会改变实参。
例如定义了两个对象,其中一个已经赋值。

#include<iostream>
using namespace std;
class student{
	public:
		int age;
	public:
		student(){};
		student(int t_age):age(t_age){};
		//复制构造函数
		student(const student &another);
		void show_age(){
			cout<<age<<endl;
		} 
};
student::student(const student &another)
{
	this->age = another.age;
}

int main()
{
	student s1(19),s2;
	s2 = s1;
	s1.show_age(); //19
	s2.show_age(); //19
	
	//修改s1看s2是否会因此改变
	s1.age = -1;
	s1.show_age(); //-1
	s2.show_age(); //19 s2的值没有被改变
	
	return 0; 
	
}

下面的例子说明了当形参是引用的时候,形参的改变将会导致实参的改变,但如上所示,不针对复制构造函数中的参数是 对象引用 的情况

#include<iostream>
using namespace std;
void change(int &x)
{
	x = -1;
}
int main()
{
	int a = 10;
	int &b = a;
	change(b);
	cout<<"a = "<<a<<endl; //-1
	cout<<"b = "<<b<<endl; //-1

	return 0;
}

变换构造函数

当构造函数只有一个参数时,也叫变换构造函数,这里变换的含义应该是: 我传入一个参数,就可以生成一个对象,一个参数变换成一个对象;感觉特意命名这种构造函数多此一举,了解一下就行。要区分开变换构造函数和变换函数。
有两种调用方式:
强调一下,没有参数的构造函数叫默认构造函数,没有定义构造函数的时候,系统将自动生成一个空的默认构造函数。只有一个参数的构造函数叫变换构造函数。

//dt1 dt2是两个对象
//对象复制时候的两种写法
classname dt2=dt1;
classname dt2(dt1);

自动调用复制构造函数将dt1对应的数据赋值给dt1
如果没有定义复制构造函数,系统会自动创建一个带参数的默认复制构造函数,功能是完完整整的传递数值,复制构造函数会自动创建。

当需要在复制的时候额外处理一些别的事情的时候,需要自定义复制构造函数,用默认的只能简单的复制。

//非默认,不只是一个参数的复制构造函数
classname(const classname &another,int n=1){
  if(n<1) n=1;
    ...
}

注意:当一个类中包含指针变量时,这时如果利用默认复制构造函数来复制对象,默认复制构造函数仅仅是将指针变量中的地址(指针值),复制到另一个对象中去,这样两个对象的指针值都是相同的,都指向同一块存储区域,在多数情况下是不方便的,此时必须自定义复制构造函数。在该构造函数中重新开辟一个空间再进行赋值。

浅复制和深复制

浅复制:默认复制构造函数只是将指针值进行复制,当其中一个指针所指的内容改变的时候,意味着另一个指针(同样的指针)所指空间内容随之改变。所谓浅复制意思是没有复制一个实体出来,操作的还是同一个存储空间。
相反地,两个指针分别指向不同的内存空间,这种复制效果为深复制,深复制:各自有各自的空间。

调用复制构造函数的三种情况:
1 . 利用已有类的对象来初始化另一个类的对象(同一个类的对象之间相互赋值)
2. 函数形参是类对象,将实参传递给形参时,将自动调用复制构造函数。如果形参是类对象的指针或对象引用,并不会调用复制构造函数。
这也解释了为什么复制构造函数的参数必须是对象的引用而不是对象,因为如果是对象的话,将对象实参传递给复制构造函数的时候还需要调用复制构造函数,这就产生了无限循环递归调用。因此,复制构造函数的参数需要定义为对象的引用。
3.函数的返回值是类对象,在执行返回语句时将自动调用复制构造函数,函数返回值是类对象的引用或者指针时不会调用复制构造函数。
总结一下就是:初始化的时候,参数传递的时候(包括传参给函数,以及函数传出来)

变换函数
用于将对象中的一个值返回出来,变换函数的定义形式 注意不要忘了operator关键字

operator 变换的类型(){return 返回值;}
//例如
operator int(){return age;}

注意:变换函数用于返回一个值,变换构造函数用于初始化。是完全不同的概念。
一旦定义了变换函数,就可以像 int idt = dt; 一样来调用变换函数了(系统自动调用),其中dt是一个类对象。如果没有定义变换函数,这种调用就是错误的。另外变换函数中也允许做一些别的操作。

operator long(){return (x*100+y);}

例如:

#include<iostream>
using namespace std;

class student{
	public:
		int age;
	public:
		student();
		student(int t_age):age(t_age){};
		void show_age(){cout<<"age = "<<age<<endl;};
		//变换函数
		operator int(){ return age;};
};

int main()
{
	typedef student stu;
	stu s1(20);
	int a = s1;//调用变换函数 
	cout<<"a = "<<a<<endl; //a = 20 
	return 0;
}

如下情况也是可以的

classname dt(100,200);
long ldt;
ldt=long(dt);//函数型的强制变换(C++语言特有),实际上是调用变换函数
ldt=(long)dt;//强制型的显式变换

注意,上面的强制类型转换,只能转换构造函数中第一个初始化的数据。比较玄学,了解一下算了,一般也没人这么用吧?

变换函数中不能有参数且必须包含return语句,同时需要注意访问权限。

静态数据成员和静态成员函数(类数据成员和类成员函数)

注意:构造函数和析构函数不能定义为静态的。

所谓静态数据成员就是该数据不是和某个对象相关联的,而是所有的对象都具有的。这些特定的数据成员的值都是不会改变的,对该类的所有对象都是一样的。静态数据成员和静态成员函数是所有的对象共享,而不是创建一个对象就拷贝一个。目的是为了提高内存的利用率。
类是具有共性的对象的抽象模型。
当某个函数被定义为静态的时候,这个成员函数就不再具有this指针,它是属于类的。
定义的时候在前面加上static关键字

因为静态成员函数没有this指针,所以不能对一般的数据成员进行操作只用于对静态数据成员进行处理

静态成员函数既可以通过已生成的对象来调用,也可以通过作用域限定运算符来调用。

静态数据成员主要用于设定对所有对象都共享的数据,设置静态成员函数的主要目的是为了提高内存的利用率
静态成员函数不能对一般的数据成员进行操作,只能作用于静态数据成员
静态成员函数不能访问普通成员变量/函数,需要通过对象间接访问
静态成员函数可以通过类名来访问,这是一般成员函数所不具备的。

注意:不能在类内部初始化静态成员变量。而且在外部初始化还要声明数据类型,相当于重新定义了,而类内部的那个只是声明,并没有分配存储空间。

int student::master = 0;
student::master = 0; //[Error] 'master' in 'class student' does not name a type
static int master = 0; //报错,不能在类内部声明的时候初始化
//[Error] ISO C++ forbids in-class initialization of
// non-const static member 'student::master'

只能在类的外部初始化

student::master = 0; //初始化student类中的静态成员变量master

类的静态成员函数只能直接访问该类的静态成员函数和静态数据成员,不能直接访问非静态数据成员,如果要访问:需要通过对象来访问,而且只能通过类的对象来访问。

class classname
{
  static int ct; //声明一个静态数据成员,此时并没有分配存储空间,如果初始化将会报错
  int x; //普通成员变量
  public:
  void count(); //普通成员函数
static void f(classname a); //静态成员函数
};
int classname::ct;//在类的外部初始化,注意不要忘记数据类型的声明:int
void classname::f(classname a){
  cout<<ct;//正确,可以访问静态数据成员
  cout<<x;//错误,不能直接访问非静态数据成员
  cout<<a.x;//正确,通过类来访问
  count();//错误,不能直接调用
  a.count();//正确,通过类间接访问
}

指向对象的指针

class test {};
test *p = new test;//new 一个对象
//如果有带参数的构造函数
class test2{
	public:
	int age;
	test2(int t_age):age(t_age){};
}
test2 *point = new test2(21); //new 一个对象,并初始化

this指针
this 指针中存放的是当前对象的地址,不论成员函数是内部定义还是外部定义的,系统将自动将当前对象的this指针传递给成员函数。这种传递方式是自动的。
如果一个类的成员函数是采用外部定义的(类内部声明,具体实现在类外部),则在此函数的定义中也可以直接通过数据成员的名字来访问此数据成员,这是因为此时系统将this指针传递给了这个函数,所以才可以访问

//假设va,vb是类的数据成员
void classname::disp(){
  cout<<va<<endl; //直接访问数据成员,该数据成员是调用这个函数的对象的数据成员,暗含了this指针
  cout<<vb<<endl;
}
//实际上是:
void classname::disp(){
  cout<<this->va<<endl;
  cout<<this->vb<<endl;
}
//两种写法都是等价的
//当形参名和类的数据成员同名的时候,必须用this指针区分参数
void set_age(int age)
{
	this->age = age; //age 是类中的一个数据成员
	//age = age 错误,无法区分
}

友元函数,友元类
定义友元函数和友元类是为了方便非成员函数也能访问private型数据成员。

友元函数定义
在函数前面加上friend关键字,函数说明在类中,函数定义在类外的时候注意该函数是并不是成员函数(可能是别的类的函数,或者是全局下的函数)。也就是说函数前有friend修饰的一定不是成员函数。这样该函数就可以直接访问成员数据了,如果不是友元,需要通过别的成员函数来访问数据成员(曲线救国,比较麻烦)。友元函数可以出现在类中的任何位置,其作用都是一样的,友元函数不是类的成员函数,就算定义在类中(就算把实体也定义在类中),也不能是该类的成员函数。
注意:派生类中的友元函数对其基类不起作用,不能访问基类的private成员。
一个类中声明的友元函数可以是来自另一个类中的成员函数。意思是说派生的类承认这个函数是我的朋友,但是派生类所继承的基类并不承认,你交朋友,与我何干?

友元类
当一个类是另一个类的友元类的时候,这个类中的所有成员函数都 可以访问另一个类所有的数据成员和成员函数,包括private成员。在类的内部声明一个类,并且加上关键字friend,同时在外部定义具体的friend类。如果要在一个类中声明友元函数,被声明为友元函数的成员函数必须定义在该类的前面,也就是这个成员函数所属的类在该类的前面定义,里面的成员函数可以只是先声明。你都还没定义,我怎么和你交朋友呢?一点诚意都没有。

class A{...} //先声明一个类A
class B{
  ...
  public:
     ...
     friend class A;//声明A是B的友元类,A可以放问B中所有成员,包括Private
     //给A发放通行证
}

注意:只有当某个类中的成员函数的定义都完整给出之后才能定义该类的对象。

问题:友元类的定义是相对的么?如果A是B的友元,那么是否意味着B也是A的友元,在A中定义B是友元和在B中定义A是友元是否是等价的呢?
解答:从上面的定义理解来看,有A类和B类,在A类中声明了B类是友元(这是友军),那么就是给了B类一个通行证,B类可以访问A类的数据库了。但不是意味着B类也给A类发了通行证,不等价于A类也可以访问B类的数据库。所以这并不是平等的交易

当然对于友元函数是确定的,在A类中仅仅只给B类的某些个体发了通行证(在A类中声明了B类的某个函数是友元函数),也就是这个个体有访问A类中所有成员的权力。

注意:对于友元类的声明,可以先声明,定义在后。但是对于友元函数,必须将这个友元函数的家长(它所属的类)完整地定义(当然在这个家长中,它和它的兄弟可以先声明我们属于这个家,具体的定义可以放在类的外部)在前面才可以。所谓完整,就是名义上大家都在。
总之无论如何调用的时候要找得到。定义对象的时候类的定义已经完整,声明友元类的时候该类的定义要已经给出,声明某个类中的友元函数的时候,该友元函数的定义要已经给出。

运算符的重载
详细请看参考链接:https://www.cnblogs.com/zpcdbky/p/5027481.html
以及:https://www.cnblogs.com/ZY-Dream/p/10068993.html
对已有的运算符重新定义,赋予另一种功能。运算符的重载一般是在类中定义的,这样就可以在该类中使用已重载的运算符进行相应的操作。
这里利用了一个函数 operator()函数,一般定义的时候:
函数返回类型 operator(要重载的运算符)(参数列表);#这是声明
例如:
#假设已经有一个类的定义 class A
A operator+(A &a);#这是声明
A A::operator+(A &a){#这是具体定义

}#第一个A是函数返回类型,第二个A是该函数属于A类,第三个A是函数形参类型。
在operator+()函数中可以自动得到指向当前对象的this指针,因此只需要运算的另一个参数即可。可以调用该函数或者直接使用重载后的运算符。c=a.operator(b) //c=a+b
*operator函数的重载(重难点)
注意这里标题的意思是对函数重载,而不是上面的对运算符重载,这个函数是operator函数。
对函数重载的条件是:参数类型或者个数不同,函数名是相同的,返回类型可以不相同,毕竟这是两个函数了,系统根据参数个数和类型来选择调用哪个函数。
我们定义了operater+()函数用来实现运算符的重载,可以将两个对象相加,假如我们还想实现c=a+100这样的操作,也就是c=operator+(100)这样的操作,就需要对operator+()进行函数重载

类的友元是operator函数
如果将operator函数定义为类的成员函数,那么就算重载之后运算符的左边也只能是对象,而不能是数值。为了能够进行类似c=2+a的运算,可以把operator函数定义为类的友元函数。

class fraction
{
  long numera;
  long denomi;
  long gcd(long a,long b)//求最大公约数
  {
    long wk;
    while(b!=0)
    {
      a=a%b;
      wk=a;a=b;b=wk;
    }
  }
  public:
  fraction operator+(fraction &x);//实现c=a+b等价于c=a.operator(b);
  fraction operator+(long dt);//实现c=a+2等价于c=operator+(2);
  friend fraciton operator+(long dt,fraction &x);
  //实现c=2+a等价于c=operator+(2,a);
};
fraction fraction::operator+(fraction &x){
  long num1,num2;
  num1=numera*x.denimo+denimo*x.numera;
  num2=denimo*x.denomi;
  return fraction(num1,num2);
}
fraction fraction::operator+(long dt){
  return fraction(numera+denomi*dt,denomi);
}
fraction operator+(long dt,fraction x)
{
  return fraction(dt*x.denomi+x.numera,x.denomi);
}

注意:当将operator函数作为友元函数时,应该保证有一个参数至少是类的对象,否则若两个参数都是数值2+3就无法判断是正常的2+3还是调用这里的函数operator+(2,3);将会出现编译错误。
将operator+()定义为友元函数,是为了能访问到类中的所有数据成员

fraction & fraction operator+(long dt){
  return  fraction(numera+denomi*dt,denomi);
}

这里不能返回引用,这是因为该函数的返回值是利用构造函数创建的临时局部对象,而局部对象是不能返回其引用或者指针的,这是因为函数调用结束后,这个局部对象就会释放。类似于在一个函数中,int i; return &i;是错误的一样。

const对象(✔)
同一般的const变量一样,可以定义const对象,这样的对象初始化之后就不能改变了。
一旦定义成const对象之后,此对象中的所有成员函数都将被禁止使用。就算是那些不会改变数据成员值的函数也会牵连。(因为编译器无法判断是否会有改变的可能,所有一视同仁都禁止使用),为了区分开,或者说为了能使用那些不会改变数据成员值的函数,在定义的时候加上const关键字即可。
定义形式需要注意:const关键字是加在括号和定义之间的,外部定义的时候两处都需要const关键字。

#在类内部定义

class A {
  void function() const {...}
};
#在类的外部定义
class A {
  ...
  void function() const;
};
void A::function() const{...} 

注意:在函数上附加const关键字并不是表示将什么变成const型,而是表示const对象也可以使用这些函数。对于会改变数据成员值的函数来说,加上const也不能改变事实:无法调用,并且编译器会指出错误。当一个函数参数被定义为const对象时,在函数体内只能调用带有const关键字的函数。
那么问题来了,const变量有什么特点??
*int const 和 const int * 有什么区别
https://www.cnblogs.com/bencai/p/8888760.html
const int p; p is a int const. p 是一个整型常量。
const int *p; p is a point to int const. p是一个指向 整型常量的指针,p 是一个指针,p 所指内容可变,可以指向别的内容,但是所指的内容都必须是整型常量
int const *p; p is a point to const int. const int 和 int const 是一样的,同上。一般写成第一种
int *const p; p is a const point to int. p 是一个const 指针,指向int型。
const int *const p; p is a const point to int const. 同上,p是一个const 指针,指向int const.
变换一下: int const const p; 同上等价。
总结:将const int(或者int const) 和const p看成一个整体。那么就将 const p理解成(const p),那么这就是一个const 指针,具体指向什么内容,看前面的修饰,是int 还是const int(或int const), 如果仅仅是
p,那么就是一个普通指针,具体指向什么内容也看前面的修饰,是int 还是const int (或int const)

故而有: const int *const p;
int const *const p;(不是习惯写法)
int *const p;
int *p;
const int *p;
int const *p;(不是习惯写法)
const int p;
int const p;(不是习惯写法)

如何区分const 指针和一般指针?
指向常量的指针(point to const)不能用于改变其所指对象的值
const指针表示指针本身是一个常量。cosnt指针必须初始化,并且一旦初始化,const指针的值就不能改变了。

不管是const指针还是一般指针,只要指向一个常量,也就是前面的修饰带了const的,那么就不能通过这个指针试图改变这个常量的值。
如果是const 指针,也就是*const p类型的,不管是指向变量还是常量(int or const int),这个指针都是常量指针,指针的内容(地址,所指空间)就不能修改了,但是并不意味着所指对象也不能修改,如果所指对象没有const限制,那么所指对象作为变量是可以修改的。

类的嵌套定义
//一般格式
class A
{
  ...
  #在A类中可以直接用B类名来定义对象。
  B b;#定义了一个B类的对象b供A类使用。
  class B
  {
    ...
    int function();#正常定义一个成员函数,但是如果是外部定义,请看下面。
  }
}

需要注意:B作为A的内嵌类,是不能直接调用A的成员函数和数据成员的,需要利用A类的指针或引用间接调用。同样的,A类也不能直接调用B类,没有任何特权。但是public成员是可以的。private成员只能允许该类的成员函数和友元访问。
使用内嵌类时,需要指定完整的类名:
A::B b; #定义了一个B类的对象b
内嵌类成员函数的外部定义需要两次使

用作用域限定运算符
int A::B::function(){
…#针对上面的function()函数
}

当类的数据成员是类对象或常量
类的数据成员是类对象
对类中的对象成员的初始化需要在构造函数头部的后面用冒号隔开给出。(这是硬性要求么?好像是的)
如果需要用默认构造函数来初始化,就必须在类中定义默认构造函数,否则编译会出错。(不是说会自动生成?)实际上只有默认复制构造函数才是自动生成的。
解答:默认构造函数是指没有参数的构造函数,不能缺省的。可以缺省的是默认复制构造函数,如果没有定义,系统将会自动定义一个将数据完整地进行复制。
当类的成员中包含对象时,调用构造函数和析构函数是有顺序的,根据定义的对象的顺序执行他们所属类的构造函数最后再执行本类的构造函数,析构函数的执行正好相反

class outerA{
  innerA a;
  innerA b;
  innerB c;
  innerB d;
  public:
  outerA();
  ~outerA();
  ...
};
void main()
{
  outerA x;
}

按顺序执行a b c d的构造函数,然后是x的构造函数,x的析构函数,然后是d c b a 的析构函数。

类的数据成员是常量
对常量的初始化需要用构造函数来进行,并且需要在初始化列表中进行,而不能赋值进行。
可以将常量的值作为构造函数的参数来进行设置,这样就可以为每个对象设置不同的常量值了。
但是就算如此,也必须用冒号写在初始化列表中

class A
{
  const int iconst;
  const double fconst;
  public:
  ...
}
A(int a , double b):iconst(a),fconst(b)
{
  ...
}

注意当类中有const数据成员,该类的对象之间是不能相互赋值的,因为const数据对象是不能被改变的。如果希望能赋值,就要对=进行重载,只对非const数据进行相互赋值,这样就能避免改变const对象。
对象数组
C++中5种基本数据类型:int char float double bool void
#定义格式
类名 数组名[常量表达式];//这里的类名实际上是数据类型的名称,定义一个类相当于定义一个新的同int double同地位的数据类型。
假如有:class A
那么 A a[10];a是具有10个A类型对象的数组
在定义对象数组之后,系统都会调用构造函数为每一个元素初始化。对对象数组成员的访问格式:
数组名[下标].成员名
初始化的几种方式
1.通过初始值表进行初始化
A a[3]={A(1),A(2)};
A a[4]={A(),1,2,3};//当存在只有一个参数的构造函数的时候可以这样做,当然也需要默认构造函数。
2.当没有显式地赋初值的时候:通过默认构造函数初始化,如果此时没有定义默认构造函数,将会发生编译错误。

***问题:不是说会系统会自动构造默认函数么?
解答:每一个类中都必须要有构造函数,如果程序中没有定义构造函数,则系统将自动生成一个默认构造函数,将每个数据设置成一个随机数。最好自己定义一个默认构造函数,哪怕什么也不做都好。
如果类中没有定义复制构造函数,则系统将自动生成一个带有一个参数的默认复制构造函数,这样即使没有定义复制构造函数,程序也能正常运行。

3.前几个元素通过初始化表,其余元素通过默认构造函数。
4.当需要利用变换构造函数(只有一个参数)的时候,初始化表中可以只给出一个参数。
#上面等价于
A a[3]={1,2,A()};
指向类的成员的指针***
C++可以定义指向类的对象的指针,也可以定义指向类的成员的指针,包括指向类的非静态成员的指针和指向类的静态成员的指针。这个指针可以指向数据成员,也可以是成员函数。
函数名就是该函数的地址,所以有对于已经定义的指针的赋值有如下区别:
已经定义的指针:类型说明符 类名::指针名; 这里的表示定义了一个指针变量
将数据成员赋值给指针: 指针名=&类名::数据成员名;

定义指向函数的指针:类型说明符 (类名::*指针名)(函数形参列表);
将成员函数赋值给指针:指针名=类名:成员函数名;

但是,还是不能使用,因为访问成员需要类的对象来访问,所以这里还需要借助类的对象,首先创建一个对象,然后 对象名.指针名=200; 这里的是取指针内容运算符。

指向类的非静态成员的指针
定义格式如下:
类型说明符 类名::*指针名;
类型说明符是所指向的数据成员的类型名。
指针名=&类名::数据成员名;

//这样赋值后的指针还不能使用,只有通过对象才能访问类中的数据成员
//假设有类A, 有数据成员 int x;
int A::*p;//定义一个指向类的指针p
p=&A::x;//将数据成员x的地址赋值给p
A a(10);//创建一个对象
a.*p=200;//相当于a.x=200;

指向类的非静态成员函数的指针定义
类型说明符 (类名::*指针名)(形参列表);#定义一个指针
指针名=类名::函数名;#将指针指向函数
类名 对象名;#创建一个对象
(对象名.*指针名)(实参列表);#与对象结合才能使用

举个实例
class A
{
  int ct;
  public:
  void set(int a);
  ...
};

int A::*p;
p=&A::ct;
A a;
a.*p=200;

int (A::*p)(int);
p=A::set;
A a;
(a.*p)(200);

指向类的静态成员的指针
类的静态成员有什么特点?
静态数据成员和成员函数是所有的对象共有的。所有的对象都可以调用。

根据类的静态成员的特点,其指针的定义同一般指针的定义是相同的。不用复杂的指定类名。
#定义格式如下:
类型说明符 *指针名;
指针名=&类名::静态数据成员名
*指针名=x;#调用
x=*指针名;#调用
指向类的静态成员函数的指针
类型说明符(*指针名)(参数表);
指针名=类名::静态成员函数名;
指针名(实参列表);#调用
或者:
(*指针名)(实参列表);#调用
举个实例:

class A
{
   static int ct;
   static void set(int a);
   ...
};
int *p;
p=&A::ct;#or int *p=A::ct;
*p=200;

void (*p)(int);
p=A::set;
p(200);#or(*p)(200); 
  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值