C++小甲鱼学习笔记
语法篇
C++内存分配情况
- 栈:由编译器管理分配和回收,存放局部变量和函数参数,一般在函数中
- 堆:用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。
- 自由存储区:自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。new基于malloc。
- 全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量和静态变量
- 常量存储区:存储常量(const),一般不允许修改
- 代码区:存储程序的二进制代码
new/delete, malloc/free区别
这两个可以用来在堆上分配和回收空间。
前者是操作符,后者是库函数
执行new时:
- 分配未初始化的内存空间(malloc)
- 使用对象的构造函数对空间进行初始化,返回空间的首地址。
若第一步出现问题抛出std::bad_alloc异常;如果第二部出现问题则自动调用delete释放内存
执行delete时:
- 使用析构函数对对象进行析构
- 回收内存空间(free)
这已经可以看出new和malloc的区别,new得到的是经过初始化的空间,而malloc得到的是未初始化的空间 。虽然delete释放了内存空间,但是定义出来的指针仍然可以使用。
所以说new针对对象,而malloc针对空间。
因为malloc和free只是库函数,不能实现开辟空间又执行对象的构造函数,不能实现执行析构函数且回收空间,所以就有了new和delete。
const与static
const
封装,继承,多态
封装
就是把客观事物封装成抽象的类,并且类可以把⾃⼰的数据和⽅法只让信任的类或者对象操作,对不可信的进⾏信息隐藏。⼀个类就是⼀个封装了数据以及操作这些数据的代码的逻辑实体。
继承
是指可以让某个类型的对象获得另⼀个类型的对象的属性的⽅法。它⽀持按级分类的概念。继承是指这样⼀种能⼒:它可以使⽤现有类的所有功能,并在⽆需重新编写原来的类的情况下对这些功能进⾏扩展。通过继承创建的新类称为“⼦类”或者“派⽣类”,被继承的类称为“基类”、 “⽗类”或“超类”。继承的过程,就是从⼀般到特殊的过程。要实现继承,可以通过“继承”和“组合”来实现
继承有两种实现方式:
- 实现继承:实现继承是指直接使⽤基类的属性和⽅法⽽⽆需额外编码的能⼒。
- 接⼝继承:接⼝继承是指仅使⽤属性和⽅法的名称、但是⼦类必需提供实现的能⼒
多态
就是向不同的对象发送同⼀个消息,不同对象在接收时会产⽣不同的⾏为(即⽅法)。即⼀个接⼝,可以实现多种⽅法。
多态与⾮多态的实质区别就是函数地址是早绑定还是晚绑定的。如果函数的调⽤,在编译器编译期间就可以确定函数的调⽤地址,并产⽣代码,则是静态的,即地址早绑定。静态(重载)⽽如果函数调⽤的地址不能在编译器期间确定,需要在运⾏时才确定,这就属于晚绑定。动态(重写)
重载
翻译自overload,是指同一可访问区内被声明的几个具有不同参数列表(参数的类型,个数,顺序不同)的同名函数。重载不关心函数返回类型(统一为void).函数重载是为了对不同的数据类型进行相同的操作。(子类不能对基类的方法进行重载,只能重写)
void calc(int i) {
cout << i * i << endl;
}
void calc(int i, int j) {
cout << i * j << endl;
}
void calc(int i, int j, int k) {
cout << i + j + k << endl;
}
隐藏(重定义)
指派生类的函数屏蔽了与其同名的基类函数或变量,只要同名函数不管参数列表相同,基类函数都会被隐藏。隐藏后:无论派生类的内部或外部访问该成员,都是访问派生类的成员;如果要访问基类中的成员函数和变量,需要加上基类的作用域。
重写(覆盖)
指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同,派生类调用时会调用派生类函数,不会调用被重写函数。而这与有virtual修饰的虚函数有关,派生类的函数实际上是重写了父类的虚函数。即便是不加上virtual关键字,编译器也会默认其为虚函数。
class A {
public:
void prin(void) {
cout << "nonono" << endl;
}
public:
int no = 4;
};
class B:public A{
public: //重写方法
void prin(void) {
cout << "nonononononoo" << endl;
}
public:
int no = 8;
};
int main()
{
calc(4);
calc(4, 5);
calc(4, 5, 6);
B b;
B *pb = &b;
pb->prin();
cout << pb->no << endl;
pb->A::prin();
cout << pb->A::no << endl;
return 0;
}
输出:
16
20
15
nonononononoo
8
nonono
4
指针
指针所保存的是内存中的一个地址。它并不保存指向的数据的值本身,在编译的时候则是将“指针变量名-指针变量的地址”添加到符号表中,指针所指向的内容可以修改和拷贝,有const和非const之分。sizeof指针得到的是指针类型的大小
星号 * 有两种用途
第一种创建指针
第二种对指针进行解引用
int myInt = 100;
int *myPointer = &myInt; //myPointer存的是myInt在内存中的地址
*myPoint = 4; //等价于:myInt = 4;
cout << myInt << sizeof(myPointer) << endl;
输出结果为44
输出数组元素的地址
由于cout遇到char型指针会直接输出整个字符串直到遇到’\n’或者空格。
所以需要对char型指针进行类型转换要么转换成(void *)或者进行强制类型下转换,即使用reinterpret_cast()转换成unsigned long 类型。
同时也会发现数组是在内存是连续存放的。
intptr + 1就是先求intptr再求intptr+1,而(intptr+1) 是先求intptr+1再求*(intptr+1)
int intArryay[5] = { 1, 2, 3, 4, 5 };
char charArray[5] = { 'i', 'a', 'm', 's', 'u'};
int *intptr = intArryay;
char *charptr = charArray;
for (int i = 0; i < 5; i++, intptr++) //输出intArray中每一个字符的地址
cout << *intptr << "地址" << intptr << endl;
for (int i = 0; i < 5; i++, charptr++) //输出charArray中每一个字符的地址
cout << (void*)charptr << *charptr << "地址" << reinterpret_cast<unsigned long>(charptr) << endl;
输出为:
1地址007DFB74
2地址007DFB78
3地址007DFB7C
4地址007DFB80
5地址007DFB84
007DFB64i地址8256356
007DFB65a地址8256357
007DFB66m地址8256358
007DFB67s地址8256359
007DFB68u地址8256360
构造器
构造器就是初始化值
构造器和通常方法的主要区别为:
- 构造器的名字必须和他所在的类的名字一样
- 系统在创建某个类的示例时会第一时间自动调用这个类的构造器
- 构造器永远不会返回任何值
- 每个类至少有一个构造器,如果你没有在类里定义一个构造器,编译器就会使用如下语替你定义一个: ClassName::ClassName() { }
相应的在销毁一个对象时,系统也应该会调用另一个特殊方法达到对应效果:析构器。
构造函数的执行顺序
- 基类构造函数。如果有多个基类,则构造函数的调⽤顺序是某类在类派⽣表中出现的顺序,⽽不是它们在成员初始化表中的顺序。
- 成员类对象构造函数。如果有多个成员类对象则构造函数的调⽤顺序是对象在类中被声明的顺序,⽽不是它们出现在成员初始化表中的顺序。
- 派生类构造函数
自上而下
析构器
- 构造器和析构器名字一样与类名一样,只不过析构器前边多了一个 “~” 前缀。
- 析构器也不返回任何值,没有参数,不能重载,在一个类里面只有一个析构器
- 同构造器一样,每个类必须有一个析构函数,用户可以自定义,也可以通过系统自动生成。
- 一般析构函数定义为类的公有成员
- 析构函数一般写成虚函数,是为了当一个基类的指针删除一个派生类的对象时,派生类的析构函数可以被正确调用。
- 当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面存放着虚函数指针。为了节省资源,只有当一个类被用来作为基类的时候,我们才把析构函数写成虚函数!
析构函数的执行顺序
- 调用派生类的析构函数
- 调用成员类对象的析构函数
- 调用基类的析构函数
自下而上
就比如在某个类的构造器申请了一块内存,我们就必须在析构器里释放那块内存。这样就能防止内存错误。
关于构造器和析构器执行顺序这里举一个小栗子:
class Animal {
public:
string name;
Animal(string thename) {
name = thename;
cout << "Animal is" << name << endl;
}
//如果不加virtual关键字会导致子类析构器不执行
~Animal(void) {
cout << "Animal" << name << "is shutdown" << endl;
}
void eat(void) {
cout << "i am eating !" << endl;
}
};
class Pig :public Animal {
public:
Pig(string thename) : Animal(thename) {
cout << "pig is " << thename << endl;;
}
~Pig(void) {
cout << "Pig" << name << "is shutdown" << endl;
}
};
int main () {
Animal pig1("我是小猪一号");
Pig pig2("我是小猪二号");
pig2.eat();
cout << "小猪马上shutdown" << endl;
cout << "return之前还有执行" << endl;
return 0;
}
输出为:
在这里就会发现,构造器在实例化的时候最先执行,且是自上而下的,而析构器是在return 0的时候执行的,且自下而上的。
这里将main函数改为:
Animal *pig = new Pig("小猪佩奇");
delete pig;
return 0;
发现没有调用子类的析构函数,这里我们new出来一个子类对象,删除基类的指针只会调用基类的析构函数,但是子类的析构函数不会被执行,这是极其危险的。所以需要将基类的析构函数声明的虚函数即加上virtual关键字。
多态
- 多态如何实现绑定
编译时的多态性:通过重载实现(静态)重载只是一种语言特性与多态无关,与面向对象也无关。
运行时的多态性:通过虚函数实现(动态) - 编译时的多态性特点是运行速度快,运行时的特点是高度灵活和抽象
运算符重载
一般格式:
函数类型 operator 运算符名称(形参列表){
对运算符的重载处理
}
//对+操作的重载
int operator+(int a, int b) {
return a - b;
}
重载<<操作符
- 函数原型:std::ostream& operator<<( std::ostream& os, Rational f);
- 第一个输入参数os是将要向它写数据的那个流,以引用方式传递
- 第二个输入参数是打算写到哪个流里面的数据值,不同的重载<<就是根据这个所区别开来。
- 返回类型为ostream流的引用
- 如果需要访问到类的私有参数,需要声明友元函数即(friend)
举个栗子1
#include <iostream>
using namespace std;
class Box {
public:
double getVolume(void) {
return length * breadth * height;
}
void setLength(double len) {
length = len;
}
void setBreadth(double bre) {
breadth = bre;
}
void setHeight(double hei) {
height = hei;
}
//重载 + 运算符
Box operator+(const Box& b) {
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
private:
double length;
double breadth;
double height;
friend ostream& operator<<(ostream& os, Box b);
};
//因为此函数并不是Box类的成员函数,所以声明为友元函数
ostream& operator<<(ostream& os, Box b) {
os << "长为:" << b.length << "宽为:" << b.breadth <<
"高为:" << b.height;
return os;
}
int main () {
Box Box1;
Box Box2;
Box Box3;
double volume = 0.0;
// Box1 详述
Box1.setLength(6.0);
Box1.setBreadth(7.0);
Box1.setHeight(5.0);
// Box2 详述
Box2.setLength(12.0);
Box2.setBreadth(13.0);
Box2.setHeight(10.0);
// Box1 的体积
cout << Box1 << endl;
volume = Box1.getVolume();
cout << "Volume of Box1 : " << volume << endl;
// Box2 的体积
cout << Box2 << endl;
volume = Box2.getVolume();
cout << "Volume of Box2 : " << volume << endl;
// 把两个对象相加,得到 Box3
Box3 = Box1 + Box2;
// Box3 的体积
cout << Box3 << endl;
volume = Box3.getVolume();
cout << "Volume of Box3 : " << volume << endl;
return 0;
}
结果如下:
重载规则:
- C++不允许用户自己定义新的运算符,只能对已有的C++运算符进行重载。
- 除了以下五个不允许重载外,其他运算符都允许重载:
1… 成员访问运算符 .
2… 成员访问指针运算符 .*
3… 域运算符 ::
4… 尺寸运算符 sizeof
5… 条件运算符 ?: - 重载不能改变运算符操作数的个数
- 重载不能改变运算符的优先级别
- 重载不能改变运算符的结核性
- 重载运算符的函数不能有默认的参数
- 重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少有一个是类对象或类对象的引用(也就是说:参数不能全部都是C++的标准类型,这样约定是为了防止用户修改用于标准类型结构的运算符性质)
运算符重载函数实际上有两个参数,但由于重载函数是类中的成员函数,包含了一个隐藏参数:this隐式地指向了类成员对象。
重新看下之前书写的代码即可
Box3 = Box1 + Box2;
等价于Box1.operator+(Box2)
Box operator+(const Box& b) {
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
副本构造器
MyClass obj1;
MyClass obj2;
obj2 = obj1;
如果说MyClass类中有指针属性,那么当obj1删除完该指针后,obj2继续删除就会出现错误(找不到内存),这两个指针变量一样。
所以需要截获这个赋值操作,并且告诉它应该如何处理那些指针
- 重载赋值操作符
MyClass &operator = (const MyClass &rhs);(rhs表示right hand side).
因为这边使用的参数是一个引用,所以编译器在传递输入参数的时候就不会再为它创建另一个副本(否则可能导致无限递归)
又因为这里只需要读取这个输入参数,而不用改变它的值,所以我们用const把那个引用声明为一个常量
返回一个引用,该引用指向一个MyClass类的对象。对连续赋值有好处:a = b = c - 副本构造器
如果代码变成下面这样:
MyClass obj1;
MyClass obj2 = obj1;
先创建一个实例obj1,然后再创建实例obj2的同时用obj1的值对其进行初始化,而这个时候编译器会在MyClass调用一个副本构造器(copy constructor),如果找不到就会使用一个默认的。
使用
MyClass(const MyClass &rhs);
这个构造器需要一个const的MyClass类型的引用作为输入参数。因为是一个构造器所以不需要返回值。
模板
定义一个模板:
template <typename T>
或者
template <class T>
这里的class并不代表T只能是一个类。
如果编译器看不到模板的完整代码,就无法正确的生成代码。
函数模板
template <class T>
void swap(T &a, T &b) {
T tmp = a;
a = b;
b = tmp;
}
int main() {
string name = "su";
string myname = "lan";
cout << name << " " << myname << endl;
swap(name, myname);
cout << name << " " << myname << endl;
return 0;
}
为了明确表明swap()是一个函数,还可以使用swap《int》(i1, i2)来调用这个函数,这将明确的告诉编译器它将使用哪一种类型。
如果某个函数对所有数据类型都进行同样的处理,就应该把它编写为一个模板。
如果某个函数对不同的数据类型都将进行不同的处理,就应该对它进行重载。
类模板
使用模板实现一个栈
template <class T>
class Stack
{
public:
Stack(unsigned int size);
~Stack();
void push(T value);
T pop();
bool isEmpty();
T Top();
private:
unsigned int size;
unsigned int sp;
T *data;
};
template <class T>
Stack<T>::Stack(unsigned int size) {
this->size = size;
data = new T[size];
sp = 0;
}
template <class T>
Stack<T>::~Stack() {
delete[]data;
}
template <class T>
void Stack<T>::push(T value) {
data[sp++] = value;
}
template <class T>
T Stack<T>::pop() {
return data[--sp];
}
template <class T>
bool Stack<T>::isEmpty() {
if (sp == 0) return true;
else return false;
}
template <class T>
T Stack<T>::Top() {
return data[sp - 1];
}
int main () {
Stack<int> intStack(100);
cout << intStack.isEmpty() << endl;
intStack.push(1);
intStack.pop();
cout << intStack.isEmpty() << endl;
intStack.push(2);
cout << intStack.Top();
return 0;
}
输出结果为:
1
1
2
后续继续更新
出自《C++ Primier 5th》 ↩︎