一、类&对象
C++ 在 C 语言的基础上增加了面向对象编程,C++ 支持面向对象程序设计。类是 C++ 的核心特性,通常被称为用户定义的类型。类提供了对象的蓝图,所以基本上,对象是根据类来创建的。声明类的对象,就像声明基本类型的变量一样。下面是一些针对类这一概念的知识记忆点📃。
类的概念,首先是,数据的算法(操作)结合,构成一个不可分割的整体(对象)。其次,在这个整体中一些成员是保护的,他们被有效的屏蔽,以防外界的干扰和误操作。另一些成员是公共的,他们作为接口,提供给外界使用。
一个类的所有对象调用的成员函数都来自于同一代码段,在调用成员函数的时候,其实传递了相应对象的指针。类的作用域是指类定义和相应成员函数定义的范围。类型名与非类型名在同一作用域之间可以重名。
一个类的默认访问控制符为private。private和Product在访问控制的时候是一样的。
class Line// 类名
{
public: //访问修饰符
void setLength( double len );
double getLength( void );//变量
Line(double len); // 这是构造函数
private:
double length;//方法
};
// 成员函数定义,包括构造函数
Line::Line( double len)
{
cout << "Object is being created, length = " << len << endl;
length = len;
}
void Line::setLength( double len )
{
length = len;
}
double Line::getLength( void )
{
return length;
}
// 程序的主函数
int main( )
{
Line line;/声明Line类的对象line
// 设置长度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;
return 0;
}
二、构造&析构函数
类的构造函数与析构函数是类的一种特殊的成员函数,每次创建类的新对象时执行构造函数,每个删除所建立的对象时执行析构函数。下面是一些笔者在学习此知识点时的一些笔记。📋
析构函数也是特殊的类成员函数,它没有返回类型,没有参数,不能随意调用,也没有重载,只是在类对象生命周期结束的时候,由系统自动调用。析构函数以调用函数相反的顺序被调用。构造函数能够给参数配置默认值,如果为类定义了一个带参数的构造函数,还想要无参的构造函数,则必须自己定义。
为解决成员变量初始值的问题(包括成员变量为对象),可在构造函数后添加:运算符,并可以用,分开。()表示初始化的参量。在包含对象成员的类对象创建的时候,需要对象成员的创建,相应的调用对象成员的构造函数。然而,构造对象成员的顺序要看类中的声明顺序,而不是看构造函数说明中冒号后面成员初始化的顺序。
C::C( double a, double b, double c): X(a), Y(b), Z(c)
{
....
}
我们从更深层次的数据存储以及对象间相互赋值的底层原理,了解一下堆与拷贝构造函数的相关概念如下:👀
全局变量,静态数据,常量存放在全局数据区,所有类成员函数和非成员函数存放在代码区,为运行函数而分配的局部变量,函数参数,返回数据,返回地址等存放在栈区,余下的空间都被作为堆区。
new是为一个固定的数据类型在堆内申请一段空间,包含数据的构造这个过程。从堆上分配对象数组,只能调用默认的构造函数,不能调用其他任何构造函数。在默认拷贝构造函数中,拷贝的策略是逐个成员依次拷贝。拷贝构造函数的形参为类型的引用。如果你的类需要一个析构函数来析构资源,则它也需要一个拷贝构造函数(深拷贝与浅拷贝的区别是在拷贝构造函数是否申请了资源)。
在返回值为对象的时候,会产生临时对象,临时对象在整个创建他们的外部表达式范围内有效。
三、静态成员与友元
我们可以使用 static 关键字来把类成员定义为静态成员。类定义中函数原型前使用关键字 friend来声明为一个类的友元函数。下面是一些针对静态成员以及友元的概念详解。🔖
静态成员只与类相联系。静态数据成员在类声明外分配空间和初始化。一个静态成员函数不与任何对象联系,故不能对非静态成员进行默认访问。(静态成员函数和非静态成员函数的本质区别是静态成员函数没有this指针)。
友元是独立的函数,不是类的成员函数。友元的添加是为了提高效率,防止成员函数的反复调用。友元函数同时在两个类中生命,即可以访问两个类中的内容。一个类的成员函数,可以是另一个类的友元。整个类可以使另一个类的友元。友类的每个成员函数都可访问另一个类中的保护或私有数据成员。
四、继承
面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。继承代表了一种包含关系。例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。
4.1继承基本概念
多态性:函数接收的形参是基类,而实参如果是父类,则还是调用父类的重载函数(函数声明一模一样)。这样就导致函数内调用的函数会发生多态,用迟后联编或者叫滞后联编来确定到底用哪个函数。含有父类的子类,布局上This指针先指向父类数据部分,后指向子类数据部分。
若语言不能支持多态,则不能称之为面向对象的。多态性增加了一些数据存储和执行指令的代价。有一种例外,如果基类中的虚函数返回一个基类指针或返回一个基类的引用,子类中的虚函数返回一个子类的指针或者子类的引用,则C++将其视为同名虚函数而进行迟后联编。
一个类中的将所有的成员函数都尽可能地设置为虚函数对编程固然方便,JAVA语言中正是这样做的,但是会增加一些时空上的开销。对于C++来说,在对性能上有偏激追求的编程中,只选择设置个别成员函数为虚函数。
只有类的成员函数才能说明为虚函数,静态成员函数不能是虚函数(this指针加虚函数列表实现多态),内联函数不能为虚函数。构造函数不能是虚函数,析构函数可以是虚函数,并且通常声明为虚函数。
多态性虽然可以解决类的冗余,但是为了解决类的冗余而可以制造包含关系,将使问题往不可控制的方向发展。类的分解才是更好解决冗余的方法,将两个冗余的类根据冗余部分再抽象一个类,再让这两个冗余的类继承这个抽象出来的类。
具有纯虚函数的类为抽象类,其并不能构造一个具体的类。纯虚函数是在积累中为子类保留一个位置,以便子类用自己的实在函数定义来覆盖之,如果在基类中没有保留位置,则就没有覆盖。
保护的访问权限对于派生类来说是共有的,而对于其他对象来说是私有的,派生类也不能访问基类中私有的数据成员和成员函数。
4.2多重继承:
多重继承指的是一个类可以同时继承多个 类 ,比如A类继承自B类和C类,这就是多重继承。下面是对继承更多相关概念的一些详解。
在多重继承的时候,出现名称的冲突,需要说明基类:对象名.基类名.成员函数(或者成员变量)
虚拟继承的虚拟和虚拟函数的虚拟没有任何关系,虚拟继承的作用就是如果还没有基类,则加入一个基类的拷贝,否则就用有的那个基类。任何虚拟函数基类的构造函数按照他们被继承的顺序构造。任何非虚拟基类的构造函数按照他们被继承的顺序构造。任何成员对象的构造函数按照他们声明的顺序调用。类自己的构造函数。
在继承的关系中,基类的private成员不但对应用程序隐藏,甚至对派生类也隐藏。而基类的保护成员则只对应用程序隐藏,而对派生类毫不隐瞒。一个私有的或保护的派生类不是子类,程序不能接受其变为子类引用去传递给函数形参。.在无继承的类中,protected和private控制符是没有区别的。在继承中,基类的private对所有的外界都屏蔽(包括自己的派生类),基类的protected控制符对应用程序是屏蔽的,但对其派生类是可访问的。
下面是针对继承概念的一个简单C++实现:
#include <iostream>
using namespace std;
// 基类
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;
return 0;
}
五、运算符重载:
您可以重定义或重载大部分 C++ 内置的运算符。这样,您就能使用自定义类型的运算符。重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。下面的代码将针对Box加法运算符进行了重载,用于把两个 Box 对象相加,返回最终的 Box 对象。
// 重载 + 运算符,用于把两个 Box 对象相加
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;
}
下面是一些与运算符重载的相关概念详解:📑
运算符是函数,除了运算顺序和优先级不能修改之外,参数和返回值类型是可以重新说明的,即可以重载,重载的形式是,返回类型 operator 运算符号(参数说明)。运算符重载作为成员函数在声明和第一时,形式上少了一个参数,这是由于C++对所有的成员函数隐藏了第一个参数this。return *this 当返回值为引用的时候。
后增量返回的是原有对象值的临时对象。C++约定,在增量运算符定义中,放上一个整数形参,就是后增量运算符。前增量运算符返回引用,后增量运算符返回值。
转换运算符将对象转换成类型名规定的类型。operator double(){};寻找的次序:1.寻找成员函数的+运算符;2寻找非成员函数的+运算符。3.由于存在内部运算符operator +(double,double);所以假定匹配其程序中的算法。4.寻找能将实参(RMB对象)转化为double型的转换运算符operator double()。
转换运算符与转换构造函数互逆。例如类名(double)转换构造函数将double转换为RMB,而RMB::operator double()将RMB转换成double。转换运算符没有返回值。拷贝构造函数和赋值运算符,区别在类的对象是否已经存在。赋值运算符符实现之前,释放已有对象所占用的资源。
六、I/O流
C++的IO流,特指以流的方式进行输入输出的ISO/ANSI标准C++库的输入输出类库,也就是专门负责处理IO操作的一套系统。 任何需要传递的数据,都要经过这套系统的处理 。下面是我对C++的I/O流的一些个人理解。💾
写到cerr上的信息是不能被重定向的。
.I/O标准流类:头文件为iostream.h;C++的名字为cin,cout,cerr,clog。文件流类:头文件fstream.h,ofstream,ifstream,fstream。串流类:头文件strstrea.h。ostrstream,istrstream,strstream。
控制符,用流对象的成员函数控制输出的格式,用控制符。流成员函数的优点是可以返回以前的设置,便于恢复设置。
5.控制符,用流对象的成员函数控制输出的格式,用控制符。流成员函数的优点是可以返回以前的设置,便于恢复设置。cin遇到空格等分隔符就会为一个接收单元,getline为cin的成员函数,能够读取已整行的文本。cin.get不跳过任何空白字符。
6.cin遇到空格等分隔符就会为一个接收单元,getline为cin的成员函数,能够读取已整行的文本。cin.get不跳过任何空白字符。
七、模板:
模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector <int> 或 vector <string>。我们可以使用模板来定义函数和类,接下来是我记录的一些个人理解。🏢
用函数模板可以将许多重载函数简单的归为一个。要是重载函数和模板函数同时存在,则先进行重载函数的匹配,然后再寻求模板的匹配。
模板类:template <class T> class 类名{这里边就用T来表示这个数据类型};之后的里边成员函数的实现:template <class T> 返回值 类名<T>::函数名(形参)。
使用类的模板方法:(1)在程序开始的头文件说明模板的定义。(2)在适当的地方创建一个模板类的实例,编译发现正在创建一个类模板对象的时候,便会创建该模板类的定义,同时,创建对应的对象实体。(3)有了对象名,以后的使用就和通常一样,但是要记住,你规定了什么类型的模板类,在使用成员函数的时候,所赋的实参也要对应该类型。
下面的实例定义了类 Stack<>,并实现了泛型方法来对元素进行入栈出栈操作:
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>
using namespace std;
template <class T>
class Stack {
private:
vector<T> elems; // 元素
public:
void push(T const&); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const{ // 如果为空则返回真。
return elems.empty();
}
};
template <class T>
void Stack<T>::push (T const& elem)
{
// 追加传入元素的副本
elems.push_back(elem);
}
template <class T>
void Stack<T>::pop ()
{
if (elems.empty()) {
throw out_of_range("Stack<>::pop(): empty stack");
}
// 删除最后一个元素
elems.pop_back();
}
template <class T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw out_of_range("Stack<>::top(): empty stack");
}
// 返回最后一个元素的副本
return elems.back();
}
int main()
{
try {
Stack<int> intStack; // int 类型的栈
Stack<string> stringStack; // string 类型的栈
// 操作 int 类型的栈
intStack.push(7);
cout << intStack.top() <<endl;
// 操作 string 类型的栈
stringStack.push("hello");
cout << stringStack.top() << std::endl;
stringStack.pop();
stringStack.pop();
}
catch (exception const& ex) {
cerr << "Exception: " << ex.what() <<endl;
return -1;
}
}
八、异常
异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw。当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。try 块中放置可能抛出异常的代码,catch 关键字用于捕获异常。下面是笔者在学习这段内容时候的一些笔记:💊
错误一种为编译错误,另一种为运行中发现的错误,它分为不可预测的逻辑错误和可以预测的运行异常。在c++中,异常是指从发生问题的代码区域传递到处理问题的代码区域的一个对象。
.C++只理会受监控过程之异常,在try块之后必须紧跟一个或多个catch()语句,目的是对发生的异常进行处理。catch()括号中的声明只能容纳一个形参,当类型与抛掷异常的类型匹配的时候,该catch()块便称捕获了一个异常而转到其块中进行异常处理。
异常的规则:(1)任意数量的catch分程序立即出现在try分程序之后,在try分程序出现之前,不能出现这些catch程序块。(2)跟在Catch之后圆括号中必须包含有数据类型,捕获是利用数据类型匹配实现的。(3)如果一个函数抛一个异常,但是在通往异常处理函数的调用链中找不到与之匹配的catch,则该程序调用一个abort()函数终止调用。这个参数必须是严格匹配(4.)如果catch分程序执行完毕,则跟随最后catch分程序的代码(如果有的话)就被执行。
一个对象不含数据成员,则没有必要给捕获对象一个名字。抛掷的异常如果在try内,正常匹配catch语句,如果不在,则向上游的catch语句去匹配。如果上游也没有,则C++将执行默认的异常处理函数。