【C++】类和对象之基础
类的基本思想是数据抽象和封装。《C++Primer P228》
数据抽象依赖接口和实现。就是说,先从特殊到一般抽象出来事物的功能,形成接口(功能)以及成员变量(属性),而后再从一般到特殊实现一个个实例。(比如,所有的人经过抽象后,其有功能(走、跳、吃喝等)和属性(身高、体重、国别等)。而后再进行实例化,如亚洲人、欧洲人、非洲人、美洲人等)
封装是指将属性变为私有,通过接口暴漏到外部。
0 前言
本篇内容是类和对象的基础内容。包含:内联函数、this指针、友元函数、静态成员函数和变量、const、左值与右值引用。
1 内联函数
- 功能:类似于宏替换的过程,实现的是在编译阶段直接在函数调用处将函数展开。
- 用法:inline。inline用于成员函数时,可以在声明和定义处都写。(成员函数自动已经内联)
inline void print() { cout << "inline func" << endl; }
- 用途:用于频繁调用,函数短小的地方。成员函数就是内联函数。
- 注意:inline只是向编译器发送了这个请求,具体如何还是看编译器。
2 this指针
关键字:隐式存在、常量指针(顶层const)、底层const、返回this指针、友元函数、静态函数
-
this指针隐式的存在于成员函数的第一个位置参数位置,传递指向的对象。
-
this指针是常量指针,这是顶层const
T *const this
。 -
this指针可以使用底层const修饰,const放置在成员函数的函数体前,此成员函数称为const成员函数。常量对象只可以调用常量成员函数,变量对象可以调用常量成员函数(只读)和非常量成员函数。
//成员函数 void setValue(int a)const { m_a = a; //会报错,this的底层const修饰后,此对象空间的值就不能被更改 }
-
返回this指针:C++中在定义的函数类似于内置运算符的操作时应该令这个行为模仿运算符。因此需要返回当前对象的左值引用时就要对this解引用。
Product& sales(Product& other)//成员函数 { ... return *this; }
cosnt成员函数返回*this,返回值应当为const修饰的引用。(非const对象调用const成员函数,在函数内部不可修改其成员变量,但是返回的*this也是const类型的。)
const Product& sales(Product& other) { return *this; }
3 友元函数
关键词:类外普通函数、类内(friend+函数)的声明、访问类的私有成员、声明位置皆可
-
基本概念
友元函数是可以直接访问类的私有成员的非成员函数。友元函数指的是对应类的“好朋友”,所以应该在类中“注册”。 -
用法
它是定义在类外的普通函数,它不属于任何类,但需要在类的定义中加以声明,声明时只需在友元的名称前加上关键字friend,其格式如下:
friend 类型 函数名(形式参数);
友元函数的声明可以放在类的私有部分,也可以放在公有部分,它们是没有区别的,都说明是该类的一个友元函数。一个函数可以是多个类的友元函数,只需要在各个类中分别声明。来源 -
要求
- 不可传递、不可继承、可重载(要将所有的重载函数都声明为友元函数)
- 注意:可以在友元函数内部通过对象或者引用、指针等访问其私有成员,但是不代表友元函数拥有了此类的作用域!其没有this指针!
//.h class Object { friend ostream& operator<<(ostream& os, const Object& a); friend istream& operator>>(istream& is, Object& a); public; privte: int v; } //.cpp ostream& operator<<(ostream& os, const Object& a) { os << a.v; return os } istream& operator>>(istream& is, Object& a) { is >> a.v; return is }
- 用途(<< >>运算符重载)
现在见到用法,用于>> 和 << 的运算符重载(code见用法)。
4 静态成员函数与静态变量
关键字:没有this指针( 只访问静态成员 没有const volite vitual) 调用方式 (补充:static全局变量)
静态成员函数的目的:设计静态成员函数的目的是独立于object之外的存取操作在某些时候很重要。也就是说,类的某些操作并不涉及对属性的操作,或者只涉及对共享成员变量的操作。比如,抽象的人类,人的总数是共享的,不能被每个对象私有(共享成员),人有时候会做某些活动,可能就不会涉及更改人自身的某些属性值。
-
静态成员函数和变量的访问
静态成员函数和变量可以通过类名::函数/变量
的方式调用/访问,也可以通过实例.函数/变量
方式,后一种方式在内部会自动转化为前一种。
静态成员函数只能访问静态成员变量,不能访问其他成员变量,其他成员函数可以访问静态成员变量。(这是因为static没有this指针)//访问形式 a.static_func() A::static_func() pa->static_func() //内部转化 object.static_func() ===》 A::static_func()
-
静态成员变量的初始化
静态成员变量在.h文件的类中声明,而在类外部(cpp文件)中进行定义。【全局变量也是如何】//.h class A { public: static int a; } extern int g;//全局变量 //.cpp int A::a = 99; int g = 10;
-
静态成员函数的修饰
由于没有this指针,访问非静态成员变量,不能使用const、volatile、virtual修饰。 -
静态成员变量与普通成员变量的区别
静态成员变量不属于任何对象,因此类中可以定义此类的static成员变量(不完整类型),但是不可定义此类的普通成员变量,只能定义此类的指针或者引用。class ObjectFunc { public: ObjectFunc() = default; private: static ObjectFunc obj; ObjectFunc obj1;//error:不允许使用不完整类型 ObjectFunc* pobj; };
-
static与inline、explicit
关键字 功能 类内声明可用 类外定义也加上? static 类所有,所有对象共享 √ × explicit 禁止构造函数类型转换 √ × inline 声明为内联函数 √ √ 对象调用函数的总结:
- 常量或者非常量对象都可以访问公有的静态成员函数或者成员变量,
a.static_x
或者pa->static_x
(因为静态函数和变量不属于任何一个对象) - 此外,常量对象只可以访问常量成员函数(函数体前const修饰this)
- 非常量对象可以访问所有的公有成员函数。当访问const成员函数时拥有只读属性,当访问因const引用参数而形成的重载函数,优先访问非const引用参数的函数。(因为const形成重载函数需要是参数引用)
- 常量或者非常量对象都可以访问公有的静态成员函数或者成员变量,
-
(补充)全局静态变量
static修饰的全局变量仅在当前文件可见,程序运行后与其他全局变量先初始化。
static修饰的局部变量(如函数内)第一次使用时初始化,同时声明周期延长至程序结束。
static修饰的全局变量或者局部变量如果不初始化,则默认初始化为0。而局部变量若不初始化,则未定义,使用时会报错(不使用不报错)。#include <iostream> using namespace std; static int u = 99;//全局静态变量 int main() { static int i;//局部静态变量 int a;//局部变量 cout << i << endl; cout << u << endl; return 0; } /* 0 99 D:\7_工作积累\3_CPP\类与对象\x64\Debug\类与对象.exe (进程 3512)已退出,代码为 0。 按任意键关闭此窗口. . . */
5 const与成员函数
关键字:参数、this指针、对象、 可见范围、初始化
const在类中的使用
非常量对象只读的两种情况:自身调用const成员函数,或者自身传递给const参数的成员函数。
- const修饰成员函数的参数,可使得变量变为只读。
- const修饰成员函数this指针,const放置在成员函数的函数体前,变量对象调用const成员函数,在函数内部此对象只读。若返回*this,返回类型为常引用(否则报错:const限定符被丢弃)。
const ObjectFunc& exampleFunc() const { return *this; }
- const修饰对象,此对象只能调用常量成员函数。
C++与C中const的区别
-
是否分配空间(可通过指针修改)
参考文章
在C语言中,定义const常量,编译器会为其分配内存空间,因此可以通过指针的方式修改。(常量但是能通过指针方式修改,因此在C中const看作修饰变量的const)const int a = 10;//分空间 int* p = (int*)&a;//常量被修改 *p = 100; //a=100
但是在C++中,定义const常量,编译器将定义的常量与符号表中的数联系起来,在编译阶段使用符号表的数替换所有使用const常量的地方(类似于宏替换),因此const常量不可以被修改,也不占用内存空间。但是当使用取地址&或者extern修饰时,编译器会为其分配空间,这是为了兼容C,不过并不影响原来的常量的值。
const int a = 10;//与符号表联系,不分空间 int* p = (int*)&a;//给p分配一个指向的空间 *p = 100; //*p=100 a=10
-
全局const的可见范围
对于全局变量(非const),C和C++都是全局可见,在不同的文件定义同名变量,最终报错:重定义。使用时应该在源文件中定义,在头文件中使用extern声明,其他包含此头文件的文件就可以使用此全局变量。
对于const全局变量,C语言和之前一样,全局可见。但是对于C++来说,const全局变量的可见范围是当前文件,因此可以在头文件中定义const变量,而后被多个文件引用。
const与函数重载
-
const引用作参数:
成员函数的参数是否为const引用可进行重载,但是如果是非引用则const并不重载。//const引用参数重载(&需要绑定到变量上,const&可绑定到变量或者常量,&只能绑定到变量上) void func(const int& a); void func(int& b); //const非引用参数不重载(值传递,const参数可由常量或者变量初始化,非const参数也是如此,因此没有区分度) void func(const int a); void func(int a);
#include <iostream> using namespace std; class ObjectConst { public: void printConst(const int& a) { cout << "参数为const" << endl; } void printConst(int& b) { cout << "参数非const" << endl; } void printConst(int& c)const { cout << "const成员函数" << endl; } void printConst(int&& d) { cout << "右值参数成员函数" << endl; } void printConst(int&& e)const { cout << "右值参数const成员函数" << endl; } }; int main() { ObjectConst object; const ObjectConst objectConst; const int a = 1; object.printConst(a); int b = 2; object.printConst(b); int c = 3; objectConst.printConst(c); int d = 4; object.printConst(std::move(d)); int e = 5; objectConst.printConst(std::move(e)); const int f = a; cout << a << endl; return 0; } /* 参数为const 参数非const const成员函数 右值参数成员函数 右值参数const成员函数 */
const修饰变量和指针
- const修饰指针存在顶层const和底层const之分,顶层const是指指针变量自身,而底层const是指指针所指的空间为const,不可修改的。
- 书写格式:
底层const:const int *p
或者int const *p
顶层const:int * const p
顶层+底层const:const int * const p
const与typedef、#define
typedef能够为类型起别名,作用于编译阶段,所以能够进行类型检查
#define直接进行字符替换,作用于预处理阶段
typedef int* PINT1
#define PINT2 int*
const PINT1 p1;//typefdef。const修饰的是指针本身,必须初始化
const PIN2 p2; //#define 修饰的是指针指向的内存空间
6 左值与右值引用
左值:L-Value(Locate-Value,有内存空间的值)(暂不介绍)
右值:R-Value(Read-Value,只读的值)
-
什么是左值和右值?
左值常常表示有存储空间的变量,如解引用、下标运算、前置加减等;右值包含纯右值(如表达式(x+1)
)和将亡值(前置加减,局部变量)。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。(如何提高临时对象或者即将销毁的变量的利用率,降低资源的消耗是右值出现的原因) -
为何要右值引用?
C++中基于值语义的拷贝和赋值严重影响了程序性能。尤其是对于资源密集型对象,如果进行大量的拷贝,势必会对程序性能造成很大的影响。为了尽可能的减小因为对象拷贝对程序的影响,开发人员使出了万般招式:尽可能的使用指针、引用。
在程序中,常常存在临时对象。如下代码的func函数return时,调用拷贝构造函数生成临时变量,从而返回main函数。但是临时对象的生存周期只存在Quote a = func1()
这一行,而后就被销毁。
在程序中,常常存在不再使用的即将销毁的局部变量,如func
中的q1
,但是想将其(局部变量)值用于初始化别的变量,就需要通过一个临时对象。
因此,就出现了右值引用。class Quote{ public: Quote(const char* p):words(p) { cout << "单参数构造函数" <<endl; } Quote(const Quote& q) { cout << "拷贝构造函数" <<endl; } Quote(Quote&& q){ cout << "移动构造函数" << endl; } Quote& operator=(Quote&& q) { cout << "移动赋值重载" << endl; } } Quote func1() //注意:不能对局部变量返回引用 { Quote q1("ddd"); return q1; } int main() { Quote a = func();//临时变量 return 0; }
右值引用的主要特点是,其只能绑定到一个即将销毁的对象上。此对象可以是临时对象(右值,保存在寄存器),也可以是变量(左值)。
-
右值引用与std::move
右值引用可以绑定到临时变量上,主要形式为:int i = 1; int&& r1 = 1;//绑定到字面量 int&& r2 = func();//绑定到返回返回值(临时变量) int&& r3 = (i + 2);//绑定到表达式
右值引用可以绑定到左值上,但是要保证左值不再使用.(绑定到左值上可以使得左值中的资源得到利用)。下述代码中func2中return函数会调用移动构造函数,而没有调用拷贝构造函数,节省了资源。
int i = 9; int && r4 = std::move(i);
Quote&& func2() //注意:不能对局部变量返回引用 { Quote q2("ddd"); return std::move(q1); } int main() { Quote a(func2());//临时变量 return 0; }
std::move()
函数是将左值转换为右值,从而使得资源继续得到使用,而后通过移动构造函数或者移动赋值重载函数来将右值转换到左值变量中。需要注意的是,定义的右值引用变量是一个实实在在的左值变量。 -
移动构造函数与移动赋值运算符
Quote(Quote&& q){ this->words = q.words; cout << "移动构造函数" << endl; } Quote& operator=(Quote&& q) { cout << "移动赋值重载" << endl; }
【移动构造函数和拷贝构造函数的区别】拷贝构造函数需要分配新的空间,但是移动构造函数不需要分配新的空间,因此减少了资源的消耗。
-
右值的用途
- 用于函数返回局部变量,返回值定义为右值,局部变量使用move转换为右值,如func2.
- 用于移动构造函数和移动赋值运算符
-
左值与右值的重载
具体应用见const。void func(int& a); void func(int&& b);