1. 认识类与对象
(1)什么是类(class)?
类(class)是类型(type),是用户自定义的类型。为什么不叫它type,因为借用Simula语言中的class关键字。
<1>为什么要有类?
基于便利性的考虑,现实世界中物(object)通常被分为几种独立的分类。
<2>基本概念
- 类:建筑图纸
- 实例化:建造
- 对象/实例:楼房
<3>面向对象的四大特征
- 1.抽象:抽出具体事物的普遍性的本质;比如分门别类:鸟类、哺乳类、鱼类
解释:抽象出来的一般与当前目标有关的方面,叫做类或者接口,抽象包括两个方面:数据抽象(特诊:对象/类的属性,c++也叫做成员变量)和过程抽象(行为:类的方法,c++叫做成员函数) - 2.封装:把数据与处理(函数)包在一起;比如通信录(增加、删除)
解释:封装就是把属性和方法都隐藏起来,保证数据的安全性,对外我们提供接口用来给我们信赖的对象访问。如私有变量。 - 3.继承:数据与处理(函数)的传承;财富与绝技、混血儿(肤色/头发、 两种语言
- 4.多态:同一个事物(函数)的多种形态;手机键盘数字与字母、 电脑键盘功能键
<4>面向对象的五大原则
1.单一职责原则
2.开放封闭原则
3.替换原则
4.依赖倒置原则
5.接口隔离原则
2.类的定义与对象创建
(1)类的定义:与struct相似(C++)
<1>格式
class 类名{
成员变量/成员函数声明;
};
注意:class定义最后的;
一定不要忘记。
<2>构成
- 数据成员(data member)/成员变量/属性:作用:对象内部数据和状态,只能在类定义中声明,可以在成员函数中直接调用。
- 成员函数/方法:作用:对象相关的操作,可以在类内实现或类外实现。成员函数可以自己调用内部其他的成员函数。在一个类里面可以调用另一个类里面
public
的所有的成员函数。
//成员函数可以自己调用内部其他的成员函数
void Print(){
cout << name << '\t' << count << "\t¥" << price << "\t¥" << GetTotal() << "\t¥" << GetOff() << endl;
}
<3>作用域运算符:: -- 函数归属
在类中定义,在类外面进行实现。如果在类里面实现的话,不需要该格式
在类外实现,使用该格式:
返回类型 类名::函数名
如:
void Student::Print(){}
<4>访问限定符
private[默认]:私有
public:公有
protected:保护
实践中,成员变量多数情况使用private或者protected,成员函数多数情况使用public。通常,通过成员函数改变对象的成员变量。
<5>类定义与类实现分离(.h和.cpp)
声明和实现分开编写。
- 头文件
方式:#pragma once
或者#ifndef #define #endif
作用:防止头文件二次编译 - 源文件 --实现
引用头文件:include <>
(标准库)/include " "
(自定义第三方库)
<6>class与struct的区别
C++的class与struct的区别
- 1.默认的控制方式不同: struct是
public
,class是private
- 2.struct可以使用花括号
{}
来初始化,class不可以(c++98不可以,c++11可以)
struct在C和C++中的区别
1.C++的struct可以添加成员函数,而C不可以
2.C++的struct可以使用访问控制关键字(public private protected
),而C不可以。
3.C++的struct在定义对象时可以忽略,而在C中不可以。
SPos spos; // C++
struct SPos spos; // C/C++
(2)对象创建/实例化
<1>直接创建 --类作为类型定义变量 --栈上创建
//基本类型
int a=10;
int b(10);//等价于int b=10;
//类类型
class Demo{};
//创建变量格式:类名 对象名; // 创建对象,自动调用默认构造函数
Demo d;
Student zhangsan("张三",true,21);//创建对象时可以直接赋值
//创建匿名对象格式:类名(); // 创建匿名对象
Demo();
<2>动态创建 --堆上创建
//基本类型
int* p = new int;
delete p;
p = NULL;
//对象指针new可以为对象设置初始值
int* p = new int(100);
cout << *p << endl;
//类类型
class Demo{};
//类名* 对象指针 = new 类名;// 调用默认构造函数
Demo* d=new Demo;
Student* lisi =new Student("李四",false,22);//可以为对象设置初始值
delete 对象指针;
<3>动态创建数组 -- 堆上创建
//基本类型
int* pa = new int[10];
delete pa;// 只释放p[0]
delete [] pa;// 释放全部数组
//类类型
Demo* d = new Demo[10];
delete [] d;
d = NULL;
//注意:对象数组指针new不可以为对象设置初始值。
int* pa = new int[10](100);//错误:parenthesized initializer
注意:
1.空结构体与空类的大小(sizeof)为1,主要在于初始化/实例化时,编译器给变量/对象分配内存(地址),内存最小单位为1个字节。通常,sizeof(类型) == sizeof(变量)。
为什么空的class需要分配一个字节?
类的实例化就是在内存中分配一块地址,空类同样可以被实例化,每个实例在内存中都有一个独一无二的地址,因此需要一块内存来存放该地址,而c++中可以申请的最小单位就是一个字节。
3.方法
(1)构造函数
<1>语法
类名(参数){
函数体
}
<2>特点
1.构造函数的函数名与类名相同
2.在对象被创建时自动执行
3.没有返回值类型、也没有返回值
4.可以有多个构造函数(构造函数的重载),但是在调用时,会根据需要调用其中一个构造函数。
<3>构造函数
1.默认构造函数:没有参数的写法。当没有编写构造函数时,如果创建对象,系统会自动调用默认的构造函数,但是该构造函数的函数体空的,不能够达到初始化的目的,编译可能不会出错,执行时会吐核。但是如果定义了构造函数,不管是否合适,编译器都不会调用默认的构造函数。
2.构造函数的重载:使用类名相同的构造函数,并且是有参数的,有赋值功能。主要用在从外边传入已经分配好的动态内存,提供一个接口。
<4>调用时机
1.使用类直接创建对象
2.new动态创建
<5>构造函数的作用
1.给创建的对象建立一个标识符
2.为对象数据成员开辟内存空间
3.完成对象数据成员的初始化
例子:
class Array{
private:
int* data;
int length;
public:
Array(){//构造函数
data=NULL;
length=0;
}
Array(int* d,int l){//构造函数的重载
data=d;
length=l;
}
};
int main{
//调用默认的构造函数
Array temp;
//调用构造函数的重载:
int* list=new int[10];//创建了动态分配内存
for(int i=0;i<10;++i){
list[i]=i;
}
Array(list,10);//调用上面的构造函数重载,从外边传入已经分配好的动态内存。
}
(2)初始化列表
初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。
主要是性能问题,使用初始化列表少了一次调用拷贝构造函数的过程,这对于数据密集型的类来说,是非常高效的。
<1>语法
类名(参数):成员变量1(参数1),成员变量2(参数2){
//函数体
}
<2>作用
初始化非静态成员变量
<3>说明
1.必须使用初始化列表的情况:
- 常量成员:因为常量只能初始化不能赋值,所以必须放在初始化列表里面。
- 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面。
- 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。
2.初始化列表与构造函数内部成员赋值的区别: 成员变量初始化与成员变量赋值
注意:能使用初始化列表的时候尽量使用初始化列表
例子:(一般都是写成初始化列表的形式)
把上面的构造函数写成初始化列表的方式:
Array():data(NULL),length(0){}
Array(int* d,int l):data(d),length(l){}
<4>成员变量的初始化顺序
class Demo1{};
class Demo2{};
class Demo3{};
class Test{
public:
Test():d1(),d3(),d2(){}
private:
Demo1 d1;
Demo2 d2;
Demo3 d3;
int main(){
Test test;
//初始化顺序结果为:d1,d2,d3。
}
成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。一般情况定义成员变量的顺序和初始化成员列表顺序一致。
<5>代码(账单bill)
#include
#include
using namespace std;
class Record{
private:
string name;
int count;
float price;
float off;
public:
Record(string name,int count,float price):name(name),count(count),price(price),off(1){}
Record(string name,int count,float price,float off):name(name),count(count),price(price),off(off){}
float GetTotal(){
return count*price*off;
}
float GetOff(){
return count*price*(1-off);
}
void Print(){
cout << name << '\t' << count << "\t¥" << price << "\t¥" << GetTotal() << "\t¥" << GetOff() << endl;
}
};
class Bill{
private:
vector records;
public:
void Add(Record r){
records.push_back(r);
}
void Print(){
cout << "物品\t数量\t单价\t总价\t节省\n";
cout << "-------------------------------------" << endl;
float totalPrice = 0;
float totalOff = 0;
for(int i=0;i
(3)析构函数
构造函数是必须的,但是析构函数有时不需要!!,当没有申请新的空间时,没有必要编写析构函数,来释放空间,系统会自动调用系统的析构函数。
<1>语法
~类名(){
函数体
}
<2>特点
1.析构函数的函数名与类名相同
2.函数名前必须有一个~
3.没有返回值类型、也没有返回值
4.只能有一个析构函数
5.没有参数
<3>调用时机
1.对象离开作用域(作用域是以{ }开始和结束的)
2.delete
:调用delete时执行,与作用域无关。
<4>默认析构函数
类中没有显式定义的析构函数,编译器就会自动为该类型生成默认析构函数
<5>作用
释放对象所申请占有的资源
<6>析构顺序
析构顺序和声明顺序完全相反,最先构造的最后析构。
C++RAII机制:
RAII(资源的取得就是初始化,Resource Acquisition Is Initialization)。步骤:1 申请资源;2 使用资源;3 释放资源。C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。 -- 百度百科
new/delete与malloc()与free()的区别?
1.new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持
2.new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void指针转换成我们需要的类型。
3.new和delete会自动调用构造函数和析构函数;但是malloc和free不会自动调用构造函数和析构函数。
4.使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
5.new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
(4)this指针
this
是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。所谓当前对象,是指正在使用的对象。例如对于stu.show(),stu 就是当前对象,this 就指向 stu。
<1>作用域
类的内部。
<2>特点
- 类的一个自动生成、自动隐藏的私有成员
- 每个对象仅有一个this指针
- 当一个对象被创建时,this指针就存放指向对象数据的首地址
- 不是对象本身的一部分,不会影响sizeof(对象)的结果
- 不能在 static 成员函数中使用
如果成员函数形参与成员变量同名,使用
this->
做为前缀区分。
class Student{
public:
void setname(string name);
void setage(int age);
void show();
private:
string name;
int age;
};
void Student::setname(string name){
this->name = name;//创建的类中成员变量也有name,使用name=name是无效的
//它的形参是name,和成员变量name重名,如果写作name = name;
//这样的语句,就是给形参name赋值,而不是给成员变量name赋值
}
void Student::setage(int age){
this->age = age;
}
void Student::show(){
cout<name<<"的年龄是"<age< setname("jack");//必须通过->来访问
pstu -> setage(16);
pstu -> show();//jack的年龄是16
Student *pstu1 = new Student;
pstu1 -> printThis();
cout< printThis();
cout<
- 注意,
this
是一个指针,要用->
来访问成员变量或成员函数; -
this
虽然用在类的内部,但是只有在对象被创建以后才会给 this 赋值,并且这个赋值的过程是编译器自动完成的,不需要用户干预,用户也不能显式地给this
赋值。本例中,this 的值和 pstu 的值是相同的。
<3>实质:
this
实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给this
。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。
C++函数传参的三种方式:
访问内存的方式:变量,指针,引用
1.传值
//void Swap(int a,int b);
//Swap(n,m);
//等价于:int a=n;int b=m;(赋值操作,不能修改值)
2.传地址/指针
////void Swap(int* a,int* b);
//Swap(&n,&m);
//等价于:int* a=&n;int* b=&m;(把地址传进去,可以修改)
3.传引用
//void Swap(int& a,int& b);
//Swap(n,m);
//等价于:int& a=n;int& b=m;(引用,可以修改)
(5)引用(别名)
<1>语法
- 声明:
const 类型名& 对象名
/类型名& 对象名
- 使用:与对象变量、基本类型变量一样
int a=10;
int& b=a;
cout << "&a:" <<&a <<'\t'<<"a:"<
引用其实就是一个别名,
a
与b
代表的是相同的对象。一个变量可以有多个别名。
int a=10;
int& c=a;
int& d=a;
cout << "&a:" << &a <<" "<< a <
<2>何处使用引用?
- (1)函数的参数列表
//使用指针可以同样实现
void Swap(int& n,int& m){//作为参数
//cout << "&n:" << &n << "\t&m:" << &m << endl;//同一块内存
int t=m;
m=n;
n=t;
}
int main(){
int n=10;
int m=100;
//cout << "&n:" << &n << "\t&m:" << &m << endl;//和上面是同样的地址
Swap(n,m);
}
- (2)函数的返回值
//当函数有多个返回值时,return可以返回一个,其他的可以使用引用来返回和指针类似
int divide(int n,int m,int& mod){//return返回div,mod以引用的方式返回
int div=n/m;
mod=n%m;
return div;
}
int mod;
int div=divide(n,m,mod);
- (3)作为成员变量 -- 对象初始化时,必须显示初始化
对象初始化时,必须显示初始化
class Simple{
int& n;
public:
//引用成员变量必须在构造函数初始化列表中初始化
Simple(int& n):n(n){}
void Print(){
cout << "&n:" << &n << '\t' << n << endl;
}
};
int main(){
int m=10;
Simple s(m);
s.Print();
m=100;
s.Print();
}
//执行结果:
&n:0x61ff04 10
&n:0x61ff04 100
注意:如果成员变量是指针,也可以达到这种效果,但是如果是变量就不行了。
<3>引用的特点
- 1.引用必须初始化
int& b=a; - 2.引用必须使用变量来初始化,初始化常量是没有意义的。
int& b=10;(错误) - 3.引用初始化后不能修改,只能是一个变量的别名,不能修改为其他变量的别名,可以修改其值。
int a=10,c=20;
int& b=a;
//下面的例子:修改b的值就是修改a的值,并没有改变b是a别名的事实,只是改变了a或者b的值。
b=12;
//b=c;//错误。已经是a的别名了,不能再修改称为c的别名了
<4>为何使用引用?
- (1)避免对象复制
- (2)避免传递空指针
- (3)使用方便
<5>引用的作用
取代指针:都可以通过函数修改函数外部的值。
有时使用引用而不是指针:指针可能是空的,会吐核。
<6>引用和指针的区别
- 1.指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名
- 2.引用只能在定义时被初始化一次,之后不可变;指针可变;(特点3)
- 3.引用不能为空,指针可以为空;
- 4.引用使用时无需解引用
*
,指针需要解引用; - 5.
sizeof 引用
得到的是所指向的变量/对象的大小,而sizeof 指针
得到的是指针本身的大小; - 6.对于引用类型的成员变量,所属类的大小是按照指针大小计算,自身大小按照自身类型计算。(计算类的sizeof时并不计算成员函数,只计算成员变量)
//各种类型指针的大小(32位的编译器是4,64位的是8)
int* pi =&i;
char* pc=&c;
bool* pb=&b;
float* pf=&f;
double*pd=&d;
cout << "sizeof(pi):" << sizeof(pi) <
引用类型的成员变量在计算类/对象的sizeof
和单独计算时是不同的。
class Reference{
int& ri;
char& rc;
bool& rb;
float& rf;
double& rd;
public:
Reference(int& i,char& c,bool& b,float& f,double& d):ri(i),rc(c),rb(b),rf(f),rd(d){}
void PrintSize(){
cout << "sizeof(ri):" << sizeof(ri) <
(6)拷贝/复制构造函数
<1>语法
类名(类名& 形参){
函数体
}
或者:
类名(const 类名& 形参){
函数体
}
只有拷贝构造函数必须使用引用,其他所有函数使用引用只是避免对象传参时多余的拷贝构造。不使用引用会调用拷贝构造函数,但是该函数不使用引用,自己没法调用自己啊。
<2>调用时机
1.手动调用
类名 对象名; // 调用默认构造函数
类名 对象2 = 对象1; // 调用复制构造函数
类名 对象3(对象1); // 调用复制构造函数
2.自动调用
- 一个对象作为函数参数,以值传递的方式传入函数体
参数前没有加引用,且没有拷贝构造函数,会造成多次释放,解决:拷贝构造函数 - 一个对象作为函数返回值,以值从函数返回
- 一个对象需要通过另外一个对象进行初始化
- 一个对象拷贝构造,它的成员对象自动调用拷贝构造
- 子对象拷贝构造父对象自动调用拷贝构造。
<3>实例
#include
#include
using namespace std;
class Array{
public:
int* data;
int size;
public:
Array():data(NULL),size(0){
cout << "Array Default Construction" << " this:" << this << " &data:"<< data <
- 实例分析:name指针被分配一次内存,但是程序结束时该内存却被释放了两次,会导致崩溃!
- 原因:这是由于编译系统在我们没有自己定义拷贝构造函数时,会在拷贝对象时调用默认拷贝构造函数,进行的是浅拷贝!即对指针name拷贝后会出现两个指针指向同一个内存空间
- 解决:在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。
- RVO和NRVO机制:
返回值优化(Return Value Optimization,简称RVO),是这么一种优化机制:当函数需要返回一个对象的时候,如果自己创建一个临时对象用户返回,那么这个临时对象会消耗一个构造函数(Constructor)的调用、一个复制构造函数的调用(Copy Constructor)以及一个析构函数(Destructor)的调用的代价。而如果稍微做一点优化,就可以将成本降低到一个构造函数的代价,也就是将内容直接构造到左值中,中间不生成临时变量。(匿名对象)。
NRVO,即Named Return Value Optimization,有名字的返回值优化。
class BigObject{};
BigObject foo(){return BigObject(); // RVO(匿名对象)}
BigObject bar(){
BigObject localObj;
return localObj; // NRVO(有名字的对象)
}
gcc/clang自动RVO/NRVO优化,不执行拷贝构造函数,所以在执行时看不到执行了拷贝构造函数。当不忽略拷贝构造函数,可以在编译命令添加选项禁止
-fno-elide-constructors
;
VC在Debug环境下返回值执行拷贝构造函数,在Release环境下实施RVO/NRVO优化。
<4>默认拷贝构造函数
如果没有自己定义拷贝构造函数,编译器会调用默认的拷贝构造函数,和之前不一样的是(默认构造函数和默认析构函数不会做任何事情),但是默认的拷贝构造函数会执行浅拷贝,即只拷贝内存地址.
- 作用:复制一个已经存在的对象
- 本质:内存拷贝(浅拷贝)
- 解决:必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。
- 问题:
1.一个类可以有多个拷贝构造函数,加上const。
2.拷贝构造函数的参数必须是引用;
3.禁止使用拷贝构造函数:
目的:编写的类不允许其他人拷贝。
两种方法:
1)添加私有的拷贝构造函数c++98:
private:
Simple(const Simple&);//私有的拷贝构造函数声明C++98
报错:Simple::Simple(const Simple&)' is private within this context
2)拷贝构造函数=delete;
Simple(const Simple&)=delete;//C++11
报错:use of deleted function 'Simple::Simple(const Simple&)
<5>什么时候需要自己定义拷贝构造函数
- 1.三大定律(The BigThree)。
- 2.一般来说你在类中进行了new操作,你就需要析构函数,在你需要析构函数的类中,一般需要加上挎贝构造函数和赋值函数。
- 3.下面三种对象需要调用拷贝构造函数(有时也称“复制构造函数”)
1)。 一个对象作为函数参数,以值传递的方式传入函数体;
2)。一个对象作为函数返回值,以值传递的方式从函数返回;
3)。一个对象用于给另外一个对象进行初始化(常称为复制初始化)
通常的原则是:①对于凡是包含动态分配成员或包含指针成员的类都应该提供拷贝构造函数;②在提供拷贝构造函数的同时,还应该考虑重载"="赋值操作符号。
深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。
假设B复制了A,修改A的时候,看B是否发生变化:
- 如果B跟着也变了,说明是浅拷贝,拿人手短!(修改堆内存中的同一个值)
- 如果B没有改变,说明是深拷贝,自食其力!(修改堆内存中的不同的值)
深拷贝(Memberwise Copy)与浅拷贝(Bitwise Copy):
- 浅拷贝:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,对带有指针的类浅拷贝会引发 memory leak,动态分配内存的浅拷贝就是只拷贝了内存地址,会造成二次/多少次释放,解决方法:深拷贝。
- 深拷贝:深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后使这个增加的指针指向这个新的内存。
(7)赋值运算符重载函数
- 类和结构体可以直接赋值,但是数组不行。
<1>语法
类名& operater=(const 类名& 形参){
// 赋值操作
return *this;
}
只有拷贝构造函数必须使用引用,其他所有函数使用引用只是避免对象传参时多余的拷贝构造
<2>调用时机
- 赋值
<3>默认赋值运算符重载函数
- 内存拷贝(浅拷贝)
<4>理解
- 拷贝构造函数用于解决初始化等三种情况而导致的内存二次释放问题;
- 赋值运算符重载函数用于解决类的赋值问题而导致的内存二次释放问题。
- 如果没有定义赋值运算符重载,编译器会调用默认的赋值操作(浅拷贝),如果自己定义了该函数,则编译器就会按照该函数来进行。
- 禁用赋值运算符重载函数与禁用拷贝构造函数一样。
初始化和赋值的区别:
1.普通情况下,初始化和赋值好像没有什么特别去区分它的意义。int a=100;和int a;a=100之间仿佛没有任何区别,但是对于c++的类就不同了:初始化不是赋值,初始化是创建变量的时候赋予其一个初始值,而赋值的含义是把对象的当前值擦除,用一个新值替代。
2.本质:赋值和初始化的区别:Simple s;Simple t;t=s;(赋值操作)。赋值是两个对象都已经构造好了,初始化是其中一个对象还不存在。
3.出现的问题:
如果类中有指针的话,赋值操作还是会造成二次释放(浅拷贝,调用了二次析构函数),记住赋值操作不会调用拷贝构造函数,因为对象都已经存在不需要再重新构造。class k=t才会调用拷贝构造函数。
<5>实例
class Test{
private:
int* p;
public:
Test():p(NULL){//默认构造函数
cout <p){//2.删除之前申请的内存(防止之前的对象赋值过),防止内存泄漏
delete p;
}
p=new int(*(s.p));//深拷贝
cout << this<<" Operator: "<
对于类中的成员变量是非指针时,写不写赋值运算符都一样。
<6>赋值运算符重载的禁用
和拷贝构造函数的禁用一样!!!
c++98和c++11两种。
//(1)c++98
private:
类名& operator=(const 类名& 对象形参);//并且不在函数中实现
//(2)c++11
public:
类名& operator=(const 类名& 对象形参)=delete;
<7>三大定律(Rule of three/The Big Three)
如果类中明确定义下列其中一个成员函数,那么必须连同其他二个成员函数编写至类内,即下列三个成员函数缺一不可:
- 析构函数(destructor)
- 拷贝构造函数(copy constructor)
- 复制赋值运算符(copy assignment operator)
(8)友元细说
友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。
<1>作用
非成员函数访问类中的私有成员
<2>分类
- 1.全局友元函数:
将全局函数声明成友元函数。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数,是全局函数。有两种写法:
//(1)在类中只写【friend 函数声明】,不写其实现,实现在类外面。
class 类名{
public:
friend void Func(参数);
};
void Func(参数){
函数体;
}
//(2)在类中直接完成全局函数的实现,在函数名前面加上friend。
class 类名{
public:
firend void Func(参数){
函数体;
}
};
- 2.友元类:
将整个类声明为友元。类2中的所有成员都可以访问类1中的私有成员。
class 类名1{
public:
friend 类名2;
};
- 3.友元成员函数
一个类中的某个成员函数需要访问另一个类的私有成员,可以将这个成员函数写成另一个类的友元成员函数。
需要分为.h和.cpp(声明和实现)两个文件来编写,有5个文件(两个类的声明,两个类的实现,及main函数)。
注意:(1)如果一个文件需要知道类的成员信息,要include头文件
(2)如果一个文件只需要只知道类名,可以使用类的前置声明.
如果用反或者多用的话都会报错。
<3>特点
- 单向性:
类A是类B的友元,但B不一定是类A的友元。 - 不可传递性
若类B是类A的友元,类C是B的友元,类C不一定是类A的友元 - 不能被继承
父类是类A的友元,但是其父类的子类不是类A的友元。
(9)const
限定符
<1>本质
- 只读(read only)
<2>const
与变量和对象
- 格式
(1)const 类型 变量 = 初始值;
或者:类型 const 变量 = 初始值;//一般采用这种写法,const在类型的后面
int const a=2;
(2)类型 const 对象;
- 理解
1.定义时必须初始化
2.全局作用域声明的const变量默认作用域是定义所在文件
3.const对象只能调用const成员函数
4.const与宏定义#define的区别
<3>const
与指针
<4>const
与引用
类型 const& 变量=初始值
const 类型& 变量=初始值
int a=10;
const int& b=a;
b=20;//错误
a=20;//可以
引用对象的值不可以改变。
<5>const
与函数的参数和返回值
- 1.const修饰函数参数
如果函数内部不会改变参数的值,参数定义成const& 类型,否则定义成&类型。
void Func(const Simple& s){
s.Print();
}
(1)不加引用会导致调用拷贝构造函数(默认浅拷贝),造成多次释放,所以加&
(2)不加const只能接受非const成员,不能接受const成员和匿名成员,
(这就是为什么拷贝构造函数的参数要加const的原因,因为不知道传入的参数是什么)
int main(){
Func(s);//非const对象
Func(k);//const对象
Func(Simple(100));//匿名对象
}
- 2.const修饰函数的返回值
函数的返回值不能改变,常用于字符串/指针
//在上面的Simple类中编写成员函数
const int Get(){//这种情况是没有意义的,因为copy的一份返回值赋值给a。
return n;
}
int a=s.Get();
*********************************
const在字符串和指针中的使用
//在String类中编写成员函数
const char* c_str(){
return str;
}
char* s=s.c_str();//不能接收,因为返回值是const char*。
const char* s=s.c_str();//必须使用const char* 来接收,返回值是一个常量指针。
<6>const修饰的成员变量和成员函数
- 1.const修饰成员变量
(1)不能在类声明中初始化const数据成员变量或者基本类型(C++98不行),但是C++11可以。
(2)const成员变量只能在类构造函数的初始化列表中初始化
class Simple{
int m=0;//C++98不允许成员变量在类声明中直接初始化
int const n=0;
private:
Simple():n(0){}//const成员变量必须在初始化列表中初始化
Simple(int n):n(n){}
};
int main(){
Simple s;
Simple t=s;//拷贝构造,可以使用拷贝构造函数
t=s;//如果存在const成员变量,不能使用赋值运算符
}
(3)注意:在c++11中成员变量在类定义中有初始化,并且在构造函数中也有初始化,编译器会自动忽略类定义中的初始化。
(4)应用:const成员变量一般用于类定义后不可修改的信息,例如:学生学号
使用const成员变量不能省略构造函数(引用类型的成员变量相同)
使用const成员变量不能使用赋值运算符重载函数
- 2.const修饰成员函数:
(1)非const的成员函数,可以修改成员变量的值,只能被非const的对象调用
(2)const成员函数,不允许修改成员变量的值,可以被const对象和非const对象调用
class Simple{
int n;
public:
Simple(int n):n(n){}
void Print()const{//const成员函数,const成员函数
cout << n << endl;
}
void Increase(){//非const的成员函数
++n;
}
};
int main(){
Simple s(10);//定义非const对象
s.Increase();//非const对象可以调用非const成员函数
s.Print();//非const对象也可以调用const成员函数
const Simple k(20);//定义const对象
k.Print();//const对象可以调用const成员函数
//k.Increase();//const对象不能调用非const成员函数
}
(1)全局函数是不能被const修饰的,const只能修饰成员函数。
(2) 必须在成员函数的声明和定义后都加上const
<7>const修饰总结
(10)static限定符
<1>本质
- 生存周期:整个程序的生存周期。
- 作用域:属于类,不属于对象
<2>语法
- 声明
class 类名{
//静态成员变量
static 类型 变量;
//静态成员函数
static 返回类型 函数(形参列表);//在返回类型前加static
};
- 定义
//静态成员变量
类型 类名::变量=赋值;///静态成员变量必须在类外面定义初始化,且没有static
//静态成员函数
返回类型 类名::函数(形参列表){//注意:定义函数时没有static
函数体;
}
静态成员变量必须在类外面初始化,且没有static限定符。
- 调用
(1)通过类名(Class Name)调用:
类名::函数(实参列表);
(2)通过对象(Object)调用:
对象.函数(实参列表);
<3>规则
-
static
只能用于类的声明中,定义不能标示为static
- 非静态是可以访问静态的方法和函数
- 静态成员函数可以设置private,public,protected访问权限
- 静态成员变量必须在类外面定义初始化
- 静态成员变量在对象之间可以共享数据(比如实现一个数据的递增)
- 只有静态成员函数才可以使用【类名::函数名/变量名】来访问。
- 静态成员变量的生存周期和函数一样长
- 对象的大小不包含静态成员变量
<4>禁忌
- 静态成员函数不能访问非静态函数或者变量(用来修改静态成员变量的值)
- 静态成员函数不能使用
this
关键字(因为属于类,不属于对象) - 静态成员函数不能使用cv限定符(const与volatile)
<5>实例
class StaticSimple{
string name;
static int num;//行号(1)
int count;//记录对象说了多少句话
public:
StaticSimple(const string& name):name(name),count(0){}//构造函数
void Print(const string& s){//const不能修饰成员函数,因为count是递增的
cout << ++num<< " "<
<6>static总结
<7>static const/static const
限定符
static const/const static
修饰的成员变量在类初始化必须是数字类型(int和char),把char归到数值类型,因为里面存的是ascii码。
class Test{
static const int a=10;
static const char e='a';
//static const float b=3.14;//不行,必须在类外初始化
};
/*static*/ const float Test::b=3.14;//要加const,不能加static,为了规范一般加上省略。
- 总结
变量类型 | 声明位置 |
---|---|
一般成员变量 | 在构造函数初始化列表中初始化 |
const成员常量 | 必须在构造函数初始化列表中初始化 |
static成员变量 | 必须在类外初始化 |
static const/const static成员变量 | 变量声明处或者类外初始化 |
<8> 类和结构体的sizeof
- 见疑惑!!!
(11) 内联函数
inline -- 宏定义的接班人
编译时将函数体代码和实参代替函数调用语句。
<1>本质
- 内联函数的代码直接替换函数调用,省去函数调用的开销
<2>条件
- 一般用在代码比较简单的函数
<3>语法
- 关键字inline必须与函数实现/定义体放在一起才能使函数成为内联,将inline放在函数声明前面不起任何作用,通常内联函数定义在头文件中(.h),定义在实现中会报错。
inline void Tickets::Get(){//inline必须写在头文件里面
--count;
++getcount;
cout << name << " leave " << count << " have " << getcount << endl;
}
inline /*static*/ void Tickets::UpdateCount(int n){
count =n;
}
- 定义在类声明之中的成员函数将自动地成为内联函数;
- 内联是自己定义的,编译器在执行时是否当作内联,取决于编译器自己的优化。
<4>慎用内联
- 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高
- 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大
- 不要随便地将构造函数和析构函数的定义体放在类声明中
<5>理解
- C++编译器直接将函数体插入在函数调用的地方;
- 内联函数没有普通函数调用时的额外开销(压栈,跳转,返回);
- 内联函数是由编译器的一种请求,因此编译器可能拒绝这种请求。
- 宏代码片段由预处理器处理,进行简单的文本替换,没有任何编译过程;
- C++编译器能够进行优化,一些函数即使没有inline声明,也可能被编译器内联编译
(12)运算符重载
<1>为什么要进行运算符重载
运算符重载是为了解决类对象之间的运算的,通常的运算符只用于算术运算,如常量int之间,因为编译器已经定义了;而一个类的两个对象之间成员进行运算必须重新定义,让编译器在遇到对象运算时能按我们要求的进行运算,这就是运算符重载的意义,即重定义运算符.运算符重载的声明operator 关键字告诉编译器,它是一个运算符重载。
<2>语法
- 成员函数运算符重载
返回值类型 operator 运算符(参数){
函数体
}
- 友元函数运算符重载
friend 返回值类型 operator 运算符(形参列表) {
函数体
}
<3> 数学类的运算符重载(复数类)
class Complex{
private:
int real;
int imag;
public:
Complex():real(0),imag(0){}
Complex(int real):real(real),imag(0){}//单个参数的构造函数可以实现int自动转化成Complex对象
Complex(int real,int imag):real(real),imag(imag){}
}
void Print(){
cout << real << "+" << imag << "i" <
- 算数运算符(
+、-、*、/、%
)
//算数运算符,成员函数加法
Complex operator+(const Complex& a)const{//不用返回引用,对局部变量的引用是没有意义的
Complex res;//不要返回局部变量的引用,可以返回成员变量的引用
res.real=real+a.real;
res.imag=imag+a.imag;
return res;
}
//简写
Complex operator+(const Complex& a)const{
return Complex(real+a.real,imag+a.imag);//匿名对象
}
//友元加法
friend Complex operator(const Complex& a,const Complex& b){
return Complex(a.real+b.real,a.imag+b.imag);
}
测试:
Complex c1(1,2);
Complex c2(2,3);
(c1+c2).Print();//匿名对象
(c1+1).Print();//1会默认转换成复数
//c1.operator+(Complex(1)).Print();//成员函数运算符重载
//operator+(c1,Complex(1)).Print();//友元函数运算符重载
(1+c1).Print();
//1.operator+(c1);错误
//operator+(Complex(1),c1).Print();////友元函数运算符重载
- 关系运算符(
==、!=
)
//关系运算符
bool operator==(const Complex& a)const{//不加const会在后面的友元!=中报错
return real==a.real && imag==a.imag;
}
friend bool operator!=(const Complex& a,const Complex& b){//!=
return !(a==b);
}
cout << (c1!=c2) << endl;
//c1.operator!=(c2);
//opearator!=(c1,c2);
//c1.operator!=(c2,X);//不写friend表示是类的成员函数,会用.调用,
- 单目运算符(
+、-
)
//单目算数运算
Complex operator+(){//取正
return *this;
}
Complex operator-(){//取反
return Complex(-real,-imag);
}
- 前缀自增自减运算符(
++、--
)
//前缀自增运算
Complex operator++(){//前缀++
++real;
return *this;
}
friend Complex operator++(Complex& a){
++a.real;
return a;
}
- 后缀自增自减运算符(
++、--
)
//后缀自增运算
Complex operator++(int){//后缀++
Complex res=*this;
++real;
return res;
}
friend Complex operator++(Complex& a,int){//(这个是局部变量使用引用不能返回)
Complex res=a;
++a.real;
return res;
}
<4>流运算符重载
流运算符只能使用友元函数实现,因为函数的第一个参数必须是流对象,不是类创建的对象。一般在流运算符中不加endl,在外面控制回车。
//流运算符
friend ostream& operator<<(ostream& os,const Complex& c){
os << c.real << '+' << c.imag<<'i';
return os;
}
friend istream& operator>>(istream& is,Complex& c);//没有const,因为c是改变的
};
istream& operator>>(istream& is,Complex& c){
is >> c.real >> c.imag;
return is;
}
测试:
cin >> c;//等同于 operator>>(cin,c);
cout << c << endl;
cin和cout中的c是字符character的意思。
<5>中括号[ ]运算符重载(下标运算符重载)
标准库中的可以直接使用【】来打印和修改,但是自己定义的类是不能打印和修改的,因此需要重载。只有成员函数
class String{
char* str;
public:
//operator[]的返回类型通常为引用,否则返回值是不能修改的,因为他是一个常量,不是变量。
//返回值不加引用可以打印,但是不能修改其值。
char& operator[](int index){//返回值类型由成员变量的类型决定。
return str[index];
}
};
int main(){
String s2("hello");//自己定义的类必须重载[ ]
s2[0]=toupper(s2[0]);
cout << s2[0] << endl;
}
<6>小括号()的重载(函数调用运算符重载)
class Simple{
int n;
public:
Simple(int n):n(n){}
void operator()(int a){
cout << (n+a) << endl;
}
};
int main(){
Simple s(10);
s(12);//仿函数,像函数
//s.operator()(12);
输出:22
}
<7>运算符重载的规则
- 不能重载的运算符:成员运算符
.
、作用域运算符::
、sizeof
、条件运算符?:
- 不允许用户自定义新的运算符,只能对已有的运算符进行重载
- 重载运算符不允许改变运算符原操作数的个数;
- 重载运算符不能改变运算符的优先级
- 重载运算符函数不能有默认的参数,会导致参数个数不匹配
(13)构造函数形参
<1>构造函数形参的默认值
class Simple{
int n;
public:
//Simple():n(0){}
Simple(int n=0):n(n){}//加默认参数,可以使Simple t有意义,在不写其他构造函数时不会报错。
void operator()(int a){
n+=a;
}
friend ostream& operator<<(ostream& os,const Simple& s){
os<
<2>禁止单参默认转换
关键字:explicit
如果构造函数只有一个参数,使用时存在默认转换的情况。如果在单参构造函数前加上关键字explicit,可以禁止默认转换的情况。
class A{
int n;
public:
explicit A(int i):n(i){
cout << "A(" << i << ")" << endl;
}
A(const A& m){//拷贝构造函数
n=m.n;
cout << "Copy Construct" << endl;
}
A operator+(A b){
return A(n + b.n);//A(30) A(30)
}
};
int main(){
A a(10); //A(10)
A b(20);//A(20)
//错误
//1+a;//1.operator+(a);
//A operator+(const A&,const A&);//如果写的是加法的友元函数重载就可以
//下面两个会调用拷贝构造函数
a.operator+(b);
a+b;
//在不加关键字时,下面是对的,1会默认转换为A(1)为匿名对象,不会调用拷贝构造函数。
//a+1;
//a.operator+(A(1));//默认转换
//在不加explicit时,100会自动转换为A(100),下面等式可以理解为A c=A(100),不会调用拷贝构造函数。
A c=100;
}
因此:匿名对象初始化一个对象时,是不会调用拷贝构造函数的。