系统学习C++
方便自己日后复习,错误的地方希望积极指正
往期文章:
C++基础从0到1入门编程(一)
C++基础从0到1入门编程(二)
C++基础从0到1入门编程(三)
C++基础从0到1入门编程(四)
参考视频:
1.黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难
2.系统化学习C++
使用类:
(1)友元
(2)运算符重载
(3)转换函数
如果想要调用类的私有成员变量,调用类的公有成员是唯一的办法
#include
using namespace std;
class A
{
public:
void func()
{
cout << a_ << ' ' << b_ << endl;
}
private:
int a_ = 0;
int b_ = 0;
};
int main()
{
A a;
a.func(); // 0 0
}
而类的私有成员函数则无法访问
友元提供了另一访问类的私有成员的方案:
(1) 友元全局函数
(2) 友元类
(3) 友元成员函数
友元全局函数:可以访问另一个类的全部成员
#include
using namespace std;
class CGirl
{
friend void func(); // 友元全局函数
public:
string name_;
CGirl() {name_ = "BigDavid"; xw_ = 87;}
void show1()
{
cout << name_ << endl;
}
private:
int xw_;
void show2()
{
cout << xw_ << endl;
}
};
void func()
{
CGirl g;
g.show1();
g.show2();
}
int main()
{
func();
return 0;
}
友元类
在友元类的成员函数中,都可以访问另一个类的所有成员
Tip:
(1)友元关系不能被继承
(2)友元关系是单向的,不具备交换性
若类B是类A的友元,类A不一定是类B的友元。B是类A的友元,类C是B的友元,类C不一定是类A的友元,要看类中是否有相应的声明。
#include
using namespace std;
class CGirl
{
friend class CBoy;
public:
string name_;
CGirl() {name_ = "BigDavid"; xw_ = 87;}
void show1()
{
cout << name_ << endl;
}
private:
int xw_;
void show2() const
{
cout << xw_ << endl;
}
};
class CBoy
{
public:
void func(const CGirl& g)
{
cout << g.name_ << endl;
cout << g.xw_ << endl;
g.show2();
}
};
int main()
{
CGirl g;
CBoy b;
b.func(g);
return 0;
}
友元成员函数:友元成员函数可以访问另一个类的所有成员,不过对于应用友元成员函数比较繁琐
class CGirl; // 前置声明
class CBoy { ...... }; // CBoy的定义
class CGirl { ...... }; // CGirl的定义
// 友元成员函数的定义。
void CBoy::func(CGirl &g) { ...... }
#include
using namespace std;
class CGirl;// 前置声明
class CBoy // CBoy的定义
{
public:
void fun(const CGirl& g);
};
class CGirl // CGirl的定义
{
friend void CBoy::fun(const CGirl& g); // 声名友元成员函数
public:
CGirl()
{
name_ = "BigDavid";
}
private:
string name_;
void show()
{
cout << name_ << endl;
}
};
// 友元成员函数的定义
void CBoy::fun(const CGirl &g) {cout << g.name_ << endl;}
int main()
{
CGirl g;
CBoy b;
b.fun(g);
}
C++将运算符扩展到自定义的数据类型,可以让对象操作更美观。
语法:返回值 operator 运算符(参数列表);
运算符重载函数的返回值类型要与运算符本身的含义一致
非成员函数版本的重载运算符函数:形参个数与运算符的操作数个数相同
成员函数版本的重载运算符函数:形参个数比运算符的操作数个数少一个,其中的一个操作数隐式传递了调用对象
重载非成员函数和成员函数版本,两者只能二选一
Tip:
(1)返回自定义数据类型的引用可以让多个运算符表达式串联起来
(2)重载函数参数列表中的顺序决定了操作数的位置
(3)重载函数的参数列表中至少有一个是用户自定义的类型,防止程序员为内置数据类型重载运算符
(4)如果运算符重载既可以是成员函数也可以是全局函数,优先考虑成员函数
(5)重载函数不能违背运算符原来的含义和优先级
(6)不能创建新的运算符
(7)以下运算符不可重载:sizeof
,.
,.*
,::
,?:
,typeid
,const_cast
,dynamic_cast
,reinterpret_cast
,static_cast
(8)以下运算符只能通过成员函数重载:=
,()
,[]
,->
#include
using namespace std;
class CGirl
{
friend CGirl& operator+(CGirl& g, int score);
friend CGirl& operator+(int score, CGirl& g);
friend CGirl& operator+(CGirl& g1, CGirl& g2);
private:
int xw_;
int score_;
public:
string name_;
CGirl() {name_ = "BigDavid"; xw_ = 87; score_ = 30;}
void show()
{
cout << name_ << ' ' << xw_ << ' ' << score_ << endl;
}
};
CGirl& operator+(CGirl& g, int score)
{
g.score_ = g.score_ + score;
return g;
}
CGirl& operator+(int score, CGirl& g)
{
g.score_ = g.score_ + score;
return g;
}
CGirl& operator+(CGirl& g1, CGirl& g2)
{
g1.score_ = g1.score_ + g2.score_;
return g1;
}
int main()
{
CGirl g;
g + 30;
g.show(); // 60
20 + g;
g.show(); // 80
g + g;
g.show(); // 160
}
六种关系运算符:==,!=,>,>=,<,<=
可以使用非成员函数和成员函数两种版本,建议采用成员函数版本
#include
using namespace std;
class CGirl
{
string name_;
int yz_;
int sc_;
int acting_;
public:
CGirl(string name, int yz, int sc, int acting)
{
name_ = name;
yz_ = yz;
sc_ = sc;
acting_= acting;
}
bool operator==(const CGirl& g1)
{
if ((yz_ + sc_ + acting_) == (g1.yz_ + g1.sc_ + g1.acting_)) return true;
return false;
}
bool operator>(const CGirl& g1)
{
if ((yz_ + sc_ + acting_) > (g1.yz_ + g1.sc_ + g1.acting_)) return true;
return false;
}
bool operator<(const CGirl& g1)
{
if ((yz_ + sc_ + acting_) < (g1.yz_ + g1.sc_ + g1.acting_)) return true;
return false;
}
};
int main()
{
CGirl g1("Big", 1, 2, 3), g2("Liu", 1, 1, 1);
if (g1 == g2) cout << "=" << endl;
else if (g1 < g2) cout << "<" << endl;
else cout << ">" << endl;
return 0;
}
重载左移运算符用于输出自定义对象的成员变量,在实际开发中用于调试和输出日志
只能使用非成员函数版本
要输出对象的私有成员,可以配合友元一起使用
#include
using namespace std;
class CGirl
{
friend ostream& operator<<(ostream& cout, CGirl& g);
string name_;
int xw_;
int score_;
public:
CGirl() { name_ = "BigDavid"; xw_ = 20; score_ = 30; }
void show()
{
cout << name_ << ' ' << xw_ << ' ' << score_ << endl;
}
};
ostream& operator<<(ostream& cout, CGirl& g)
{
cout << g.name_ << ' ' << g.xw_ << ' ' << g.score_ << endl;
return cout;
}
int main()
{
CGirl g;
cout << g << endl;
}
如果对象中有数组,重载下标运算符[],操作对象中的数组将像操作普通数组一样方便。
下标运算符必须以成员函数的形式进行重载
语法:返回值类型 &operator[](参数);
或者const 返回值类型 &operator[](参数) const;
返回值类型 &operator[](参数);
:[]不仅可以访问数组元素,还可以修改数组元素
const 返回值类型 &operator[](参数) const;
:[]只能访问而不能修改数组元素
实际开发应该同时提供以上两种形式,这样做是为了适应const对象,因为通过const 对象只能调用const成员函数,如果不提供第二种形式,那么将无法访问const对象的任何数组元素
在重载函数中,可以对下标做合法性检查,防止数组越界
#include
using namespace std;
class CGirl
{
private:
string boys[3];
public:
string name_;
CGirl() { boys[0] = "BigDavid"; boys[1] = "qwe"; boys[2] = "Liu"; }
void show() { cout << boys[0] << ' ' << boys[1] << ' ' << boys[2] << endl; }
string& operator[](int i)
{
return boys[i];
}
const string& operator[](int i) const
{
return boys[i];
}
};
int main()
{
CGirl g;
g[1] = "LiuXueJin";
cout << g[1] << endl;
g.show();
const CGirl g1 = g;
cout << g1[1] << endl;
}
C++编译器可能会给类添加四个函数:
默认构造函数:空实现
默认析构函数:空实现
默认拷贝构造函数:对成员变量浅拷贝
默认赋值函数:对成员变量浅拷贝
(1)对象的赋值运算是用一个已经存在的对象,给另外一个已经存在的对象赋值
(2)如果类中重载了赋值函数,编译器将不提供默认赋值函数。没有重载,编译器提供默认赋值函数
语法:类名& operator=(const 类名& 源对象);
Tip
1.编译器提供的默认赋值函数,是浅拷贝
2.如果对象不存在堆区内存空间,默认赋值函数可以满足需求,否则需要深拷贝
3.赋值运算和拷贝构造不同:拷贝构造是指原来的对象不存在,用已存在的对象进行构造;赋值运算是存在两个对象,把其中一个对象的成员变量的值赋给另一个对象的成员变量。
#include
#include
using namespace std;
class CGirl
{
public:
int bh_;
string name_;
int* ptr_; // 计划使用堆区内存
CGirl() { ptr_ = nullptr; }
~CGirl() { if(ptr_) delete ptr_; }
void show()
{
cout << bh_ << ' ' << name_ << ' ' << ptr_ << endl;
}
CGirl& operator=(const CGirl& g)
{
if (this == &g) return *this; // 自己给自己赋值
if (g.ptr_ == nullptr) // 如果源对象的指针为空,则清空目标对象的内存和指针
{
if (ptr_ != nullptr) { delete ptr_; ptr_ = nullptr; }
}
else // 如果源对象的指针不为空
{
if (ptr_ == nullptr) ptr_ = new int; // 如果目标对象的指针为空,先分配内存
memcpy(ptr_, g.ptr_, sizeof(int)); // 然后,将源对象内存中的数据复制到目标对象的内存中
}
bh_ = g.bh_;
name_ = g.name_;
cout << "chongzaifuzhi" << endl;
return *this;
}
};
int main()
{
CGirl g1, g2;
g1.bh_ = 8;
g1.name_ = "Big";
g1.ptr_ = new int(4);
g1.show(); // 8 Big 0x1f9276318c0
g2.show(); // 1484781408 0
g2 = g1; // chongzaifuzhi
g2.show(); // 8 Big 0x1f9276318e0
}
重载new和delete运算符是为了自定义内存分配的细节
在C++中
使用new,编译器做了两件事情:
(1)调用标准库函数operator new()分配内存
(2)调用构造函数初始化内存
使用delete时,编译器做了两件事情:
(1)调用析构函数
(2)调用标准库函数operator delete()释放内存
构造函数和析构函数由编译器调用,我们无法控制。
可以重载内存分配函数operator new()和释放函数operator delete()
1.重载内存分配函数
void* operator new(size_t size);
2.重载内存释放函数
void operator delete(void* ptr);
重载的new和delete可以是全局函数,也可以是类的成员函数
为一个类重载new和delete,实际上仍在创建static成员函数
编译器看到使用new创建自定义的类的对象时,它选择成员版本的operator new()而不是全局版本的new()
new[]和delete[]也可以重载
#include
using namespace std;
void* operator new(size_t size)
{
cout << "QJ new" << size << endl;
void* ptr = malloc(size);
cout << ptr << endl;
return ptr;
}
void operator delete(void* ptr)
{
cout << "QJ delete" << endl;
if (ptr == 0) return;
free(ptr);
}
class CGirl
{
public:
int bh_;
int xw_;
CGirl(int bh, int xw) { bh_ = bh, xw_ = xw; cout << "GouZao" << endl; }
~CGirl() { cout << "~CGirl()" << endl; }
void* operator new(size_t size)
{
cout << "Dy Lei new" << endl;
void* ptr = malloc(size);
cout << ptr << endl;
return ptr;
}
void operator delete(void* ptr)
{
cout << "Dy Lei delete" << endl;
if (ptr == 0) return;
free(ptr);
}
};
int main()
{
int* p1 = new int(3);
cout << "p1=" << (void*)p1 << ' ' << "*p1=" << *p1 << endl;
delete p1;
CGirl* p2 = new CGirl(3, 8);
cout << p2 << ' ' << p2->bh_ << p2->xw_ << endl;
delete p2;
}
/*
* QJ new4
* 0x24957a618c0
* p1=0x24957a618c0 *p1=3
* Dy Lei new
* 0x24957a618c0
* GouZao
* 0x24957a618c0 38
* ~CGirl()
* Dy Lei delete
*/
内存池:预先分配一大块的内存空间
(1)提升分配和归还的速度
(2)减少内存碎片
#include
#include
using namespace std;
class CGirl
{
public:
int bh_;
int xw_;
static char* pool_;
static bool initpool() // 初始化内存池的函数
{
pool_ = (char*)malloc(18); // 向系统申请18字节的内存
if (pool_ == 0) return false; // 申请内存失败
memset(pool_, 0, 18);
cout << "Start address: " << (void*)pool_ << endl;
return true;
}
static void freepool() // 释放内存池
{
if (pool_ == 0) return;// 如果内存池为空,不需要释放
free(pool_); // 把内存池还给系统
cout << "Free finish" << endl;
}
CGirl(int bh, int xw) { bh_ = bh, xw_ = xw; cout << "CGirl()\n"; }
~CGirl() { cout << "~CGirl\n"; }
void* operator new(size_t size)
{
if (pool_[0] == 0) // 判断第一个位置是否空闲
{
cout << "fenpei first neicun: " << (void*)(pool_ + 1) << endl;
pool_[0] = 1; // 把第一个位置标记为已分配
return pool_ + 1; // 返回第一个用于存放对象的址
}
if (pool_[9] == 0)// 判断第二个位置是否空闲
{
cout << "fenpei second neicun: " << (void*)(pool_ + 9) << endl;
pool_[9] = 1;// 把第二个位置标记为已分配
return pool_ + 9;// 返回第二个用于存放对象的址
}
// 如果以上两个位置都不可用,那就直接系统申请内存
void* ptr = malloc(size);// 申请内存
cout << ptr << endl;
return ptr;
}
void operator delete(void* ptr)
{
if (ptr == 0) return;// 如果传进来的地址为空,直接返回
if (ptr == pool_ + 1) // 如果传进来的地址是内存池的第一个位置
{
cout << "Free first neicun\n";
pool_[0] = 0;// 把第一个位置标记为空闲
return;
}
if (ptr == pool_ + 9)// 如果传进来的地址是内存池的第二个位置
{
cout << "Free second neicun\n";// 把第二个位置标记为空闲
pool_[9] = 0;
return;
}
free(ptr);// 如果传进来的地址不属于内存池,把它归还给系统
}
};
char* CGirl::pool_ = 0;// 初始化内存池的指针
int main()
{
if (CGirl::initpool() == false) { cout << "init false\n"; return -1; }// 初始化内存池
CGirl* p1 = new CGirl(3, 8);// 将使用内存池的第一个位置
cout << p1 << ' ' << p1->bh_ << ' ' << p1->xw_ << endl;
CGirl* p2 = new CGirl(4, 7);// 将使用内存池的第二个位置
cout << p2 << ' ' << p2->bh_ << ' ' << p2->xw_ << endl;
CGirl* p3 = new CGirl(6, 9);// 将使用系统的内存
cout << p3 << ' ' << p3->bh_ << ' ' << p3->xw_ << endl;
delete p1;// 将释放内存池的第一个位置
CGirl* p4 = new CGirl(5, 3);// 将使用内存池的第一个位置
cout << p4 << ' ' << p4->bh_ << ' ' << p4->xw_ << endl;
delete p2;// 将释放内存池的第二个位置
delete p3;// 将释放系统的内存
delete p4;// 将释放内存池的第一个位置
CGirl::freepool(); // 释放内存池
}
重载括号运算符,对象名可以当作函数来使用(函数对象、仿函数)
语法:返回值类型 operator()(参数列表);
Tip:
(1)括号运算符必须以成员函数形式重载
#include
using namespace std;
void show(string str)
{
cout << "Common: " << str << endl;
}
class CGirl
{
public:
void operator()(string str)
{
cout << "CZHanshu: " << str << endl;
}
void operator()(int i)
{
cout << i << endl;
}
};
int main()
{
CGirl g;
g("asd"); // CZHanshu: asd
g(8); // 8
show("asd");// Common: asd
return 0;
}
(2)括号运算符重载函数具备普通函数的全部特征
(3)如果函数对象与全局函数同名,按照作用域规则选择调用的函数
#include
using namespace std;
void show(string str)
{
cout << "Common: " << str << endl;
}
class CGirl
{
public:
void operator()(string str)
{
cout << "CZHanshu: " << str << endl;
}
void operator()(int i)
{
cout << i << endl;
}
};
int main()
{
show("asd"); // Common: asd
CGirl show;
show("asd"); // CZHanshu: asd
::show("asd"); // Common: asd
return 0;
}
函数对象的用途:
(1)表面像函数,部分场景可以代替函数,在STL中有广泛应用
(2)函数对象本质是类,可以用成员变量存放更多的信息
(3)函数对象有自己的数据类型
(4)可以提供继承体系
可重载的一元运算符
(1)++ 自增
(2)-- 自减
(3)! 逻辑非
(4)& 取地址
(5)~ 二进制反码
(6)* 解引用
(7)+ 一元加
(8)- 一元求反
一元运算符通常出现在他们操作对象的左边
自增运算符++和自减运算符--有前置和后置之分
C++ 规定,重载++或- -时,如果重载函数有一个int形参,编译器处理后置表达式时将调用这个重载函数。
++前置 成员函数版:CGirl &operator++();
后置++ 成员函数版:CGirl operator++(int);
++前置 非成员函数版:CGirl &operator++(CGirl &);
后置++ 非成员函数版:CGirl operator++(CGirl &,int);
#include
using namespace std;
class CGirl
{
public:
string name_;
int ranking_;
CGirl() { name_ = "BigDavid"; ranking_ = 5; }
void show() const { cout << name_ << ' ' << ranking_ << endl; }
CGirl& operator++() // ++前置的重载函数
{
ranking_++;
return *this;
}
CGirl operator++(int) // ++后置的重载函数
{
CGirl tmp = *this;
ranking_++;
return tmp;
}
};
int main()
{
CGirl g1, g2;
int i = 5, j = 5;
int x = ++(++(++i));
int y = j++;
cout << x << ' ' << y << endl; // 8 5
CGirl g3 = ++(++(++g1));
cout << g3.name_ << ' ' << g3.ranking_ << endl; // BigDavid 8
CGirl g4 = g2++;
cout << g4.name_ << ' ' << g4.ranking_ << endl; // BigDavid 5
}
对于内置数据类型,如果两种数据类型是兼容的,C
++可以自动转换,如果从更大的数转换为更小的数,可能会被截断或损失精度‘。
C++不自动转换不兼容的类型,非法:int* ptr = 8;
不能自动转换,可以使用强制类型转换:int* p = (int*)8;
如果某种类型与类相关,从某种类型转换为类类型是有意义的
#include
using namespace std;
class CGirl
{
public:
int bh_;
string name_;
double weight_;
CGirl() { bh_ = 0; name_.clear(); weight_ = 8; cout << "CGirl()\n"; }
void show() { cout << bh_ << ' ' << name_ << ' ' << weight_ << endl; }
explicit CGirl(int bh)
{
bh_ = bh;
name_.clear();
weight_ = 0;
cout << "CGirl(int bh)\n";
}
CGirl(double weight) { bh_ = 0; name_.clear(); weight_ = weight; cout << "CGirl(double weight)\n"; }
};
int main()
{
// CGirl g1(8); // 常规写法
//CGirl g1 = CGirl(8);// 显式转换
//CGirl g1 = 8;// 隐式转换
//CGirl g1;// 创建对象
//g1 = (CGirl)8;// 隐式转换,用CGirl(8)创建临时对象,再赋值给g
CGirl g1 = 8.9;// 隐式转换
//g1.show();
}
Tip:
(1)一个类可以有多个转换函数
(2)多个参数的构造函数,除第一个参数外,如果其它参数都有缺省值,也可以作为转换函数
(3)在实际开发中,如果强调的是构造,建议使用explicit,如果强调的是类型转换,则不使用explicit
(4)将构造函数用作自动类型转换函数似乎是一项不错的特性,但有时候会导致意外的类型转换。explicit关键字用于关闭这种自动特性,但仍允许显式转换
explicit CGirl(int bh);
CGirl g = 8; // 不行
CGirl g = CGirl(8);
CGirl g = (CGirl)8;
(5)如果自动类型转换有二义性,编译报错
CGirl(int)的隐式转换的场景:
1.将CGirl对象初始化为int值
CGirl g1 = 8;
2.将int值赋给CGirl对象
CGirl g1; g1 = 8;
3.将int值传递给接受CGirl参数的函数
#include
using namespace std;
class CGirl
{
public:
int bh_;
string name_;
double weight_;
CGirl() { bh_ = 0; name_.clear(); weight_ = 8; cout << "CGirl()\n"; }
void show() { cout << bh_ << ' ' << name_ << ' ' << weight_ << endl; }
CGirl(int bh)
{
bh_ = bh;
name_.clear();
weight_ = 0;
cout << "CGirl(int bh)\n";
}
CGirl(double weight) { bh_ = 0; name_.clear(); weight_ = weight; cout << "CGirl(double weight)\n"; }
};
void fun(CGirl g)
{
g.show();
}
int main()
{
// CGirl g1(8); // 常规写法
//CGirl g1 = CGirl(8);// 显式转换
//CGirl g1 = 8;// 隐式转换
//CGirl g1;// 创建对象
//g1 = (CGirl)8;// 隐式转换,用CGirl(8)创建临时对象,再赋值给g
//CGirl g1 = 8.9;// 隐式转换
//g1.show();
fun(7); // 7 0
}
4.返回值被声明为CGirl的函数试图返回int值
#include
using namespace std;
class CGirl
{
public:
int bh_;
string name_;
double weight_;
CGirl() { bh_ = 0; name_.clear(); weight_ = 8; cout << "CGirl()\n"; }
void show() { cout << bh_ << ' ' << name_ << ' ' << weight_ << endl; }
CGirl(int bh)
{
bh_ = bh;
name_.clear();
weight_ = 0;
cout << "CGirl(int bh)\n";
}
CGirl(double weight) { bh_ = 0; name_.clear(); weight_ = weight; cout << "CGirl(double weight)\n"; }
};
CGirl func()
{
char c = 8;
return c;
// return 8;
}
int main()
{
// CGirl g1(8); // 常规写法
//CGirl g1 = CGirl(8);// 显式转换
//CGirl g1 = 8;// 隐式转换
//CGirl g1;// 创建对象
//g1 = (CGirl)8;// 隐式转换,用CGirl(8)创建临时对象,再赋值给g
//CGirl g1 = 8.9;// 隐式转换
//g1.show();
func(); // CGirl(int bh)
}
构造函数只用于从某种类型到类类型
的转换,如果要进行相反的转换,可以使用特殊的运算符函数-转换函数
语法:operator 数据类型();
Tip:转换函数必须是类的成员函数;不能指定返回值类型;不能有参数
可以让编译器决定选择转换函数(隐式转换),可以像使用强制类型转换那样使用它们(显式转换)
int i = girl; // 隐式转换
int i = (int)girl; // 显示转换
int i = int(girl); // 显示转换
隐式转换存在二义性,编译器将报错
在C++98中,关键字explicit不能用于转换函数,但C++11消除了这种限制,可以将转换函数声明为显式的
经常用:用一个功能相同的普通成员函数代替转换函数,普通成员函数只有被调用时才会执行
应谨慎的使用隐式转换函数。通常,最好选择仅在被显式地调用时才会执行的成员函数
#include
using namespace std;
class CGirl
{
public:
int bh_;
string name_;
double weight_;
CGirl() { bh_ = 8; name_ = "Big"; weight_ = 40.2; }
explicit operator int() { return bh_; }
int to_int() { return bh_; }
operator string() { return name_; }
explicit operator double() { return weight_; }
};
int main()
{
string name_ = "Big";
// const char* ptr = name_;
const char* ptr = name_.c_str();
CGirl g;
int a = g.to_int(); cout << a << endl; // 8
string b = string(g); cout << b << endl;// Big
double c = double(g); cout << c << endl;// 40.2
short d = (int)g; cout << d << endl; // 8
}