1.头文件和类、构造函数
c++和c最大的不同在于C++会把数据以及处理数据的函数放到一个对象objects(class)里,不同类之间不可见,类似C中结构体struct
防止头文件重复声明
ifndef complex
//当之前没有声明过这个头文件时,才进行后续的声明
define complex
(2)补充定义
(1)类定义
(3)类功能解释
endif
(1)类定义
class complex
{
public:
complex (double r=0,double i=0)//参数默认值
: re(r),im(i)//构造函数才能使用的特殊赋值写法
{}//{re=r,im=i}当然可以,但效率不高
//不带指针不需要析构函数
complex() : re(r),im(i) {}
//构造函数经常重载(同名但函数不同)
//但这两个构造complex的函数不能同时存在,
//因为第一个变量要默认值,第二个没有变量
//当编译器编译时如果没有变量,不知道该调用哪一个
double real() const { return re;}
double real(double x) {return re+x;}
//这样两个就可以
//const的含义此处不解释,下一节说
private:
double re,im;
//私有的变量,main就不能直接访问
//需要访问只能在类定义部分写一个公开的调用函数
//私有的变量只能在定义函数内部调用
friend complex
}
2.参数传递与返回值
单例设计模式singleton
可以通过把构造函数放到private里实现,这样做会实现:
外界main不能调用构造函数,所以无法创建新的对象
只有类定义函数可以创建对象,可以做到一个类只有一个对象
当函数不会改变数据时,要加上const
const还有其他的用法
const int x=2;
//表示这个变量是常量,数据无法被改变
const complex c1(1,2)
cout<<c1.real()<<endl;
//会出现问题,如果real()函数在设计时没有加入const
//在打印c1时,调用real函数,编译器会认为
//一个常量即将调用一个可能会改变其的函数,就会报错
//所以当函数不会改变数据时,要加上const
double real() const {return re;}
参数传递分为 by value 和 by reference 两种
两者区别在于by value 会将变量值传函数,机器底层就是将变量复制到函数的所占据的内存堆栈
后者只会传出一个地址,类似但不是一个指针,可以提升传递速度
大型项目推荐都使用by reference 类型的传递
此外还有,如果传递的是地址,类似引用类的传递
在函数中对变量进行修改,是会改变该变量的
如果不希望修改,需要加上const
以 double 为例,现在我们传 const double&
既然传入参数有两类,那么传出参数的返回值当然也有两类
返回值也可以带const
返回值传递也分为 by value 和 by reference 两种
by reference本质上是地址,但在使用时当做值来用a.re,而不是a->re
double& printt(double a,const double& b){
a = b + 1;
return a;
}
//输入参数,输出返回值都是采用 by reference的形式
//而且不会修改原来的b值,使用const
//其实一般不会用到double&,一般只可能对类,如complex使用
友元可以突破private的限制,直接访问类数据
friend complex& _doapl(complex*,const complex&)
同一类的不同objects互为友元,所以同一类的函数可以调用自己的private部分
但不是所有的情况都可以使用by reference 的模式,主要是const的使用要谨慎
3.操作符重载与临时对象
c++允许操作符重载,比如把+=重载
所有的成员函数其实都包含一个隐含的参数this, 表示调用这个成员函数的变量
complex& complex::operator +=(this,const complex& r){
return _doapl(this,r);
}//成员函数
c3+=c2+=c1;
//存在连续计算的可能性,所以就需要返回complex&
//如果不需要连续计算,可以不返回,
//因为引用型的调用会直接改变值,不需要再返回
complex& _doapl(complex* a,const complex& b){
a.re=a.re+b.re;
a.im=a.im+b.im;
return a;//返回a指针的值,即a所指的complex变量
}
//为什么return的是变量a,函数却可以写complex&来接收
//这在语法里是允许的,传递者无需知道接收者以refrence形式接收
使用inline 可以在类定义里简单声明,然后在类定义外部写成员函数,看起来比较清晰
全局函数与成员函数
inline complex&
complex::operator +=(this,const complex& r){
...//包含类名complex::
}//在类定义函数外面写的时候就要加上complex::
inline double
imag (const complex& x)
{
...//不含类名,是全局函数
}
inline complex
operator + (const complex& x,const complex& y)
{
return complex(x.re+y.re,x.im+y.im);
}
//typename(a),创建临时对象,就是没有名称的变量
//只能在返回到时候用,因为只能存在一行代码的时间
//这里不能用complex&,因为函数创建了一个新的对象,必须要一个新地址
//关于为什么要把operator +写成非成员函数
是因为,成员函数都有一个隐含的左值变量,函数一定是要对左值使用的
c=a+b对右边两值使用,c+=a就包含对左值的使用
特殊的操作符(<<输出)有特殊的设计方法,操作符重载必须注意
//练习负数类的书写,读,理解,写
4.三大函数(拷贝构造,拷贝赋值,析构)
当我们创建一个新的类时,对应这个类会有一系列操作函数,例如+-=
基本的数据类型都有自己的操作函数
所以我们也需要给新建的类创建操作函数
此外编译器会有默认的=函数(构造、拷贝函数),就是直接进行位位对应的复制(浅拷贝)
在之前的负数类中,不含有指针变量,全是值,所以可以使用默认的
但在含有指针的类中,需要创建一个新的变量,而不是复制一个地址,所以需要自己写(深拷贝)
class String{
public :
String (const char* cstr =0);//构造情况复杂,不采用初始化方式
String (const String& str);//拷贝构造
////////strlen和strcpy函数对字符串,字符都可以操作的
String& operator= (const String& str);//拷贝赋值
~String(); //析构函数
char* get_c_str() const { return m_data;}//不改变,中间加上const
private :
char* m_data;
}
inline //尝试将函数放入到类定义函数中
String::String(const char* cstr=0){
if (cstr){
m_data = new char[strlen(cstr)+1];
//含有隐含的变量this.m_data
//strlen表示字符串长度函数
strcpy(m_data,cstr);
//strcpy表示字符串复制函数
///////strlen和strcpy函数都是对字符串操作的
else{
m_data = new char[1];
m_data = '\0'; //创建的是一个指针,赋值时需要加
}
}
}
inline
String::String(const String& str){//const 表示在这个函数里不改变这个变量
m_data = new char[strlen(str.m_data)+1];
strcpy(m_data,str.m_data);
///////不能用字符串的长度去构建字符指针
}//没有自我拷贝构造,因为一个变量只能构造一次
inline
String& String::Operator= (const String& str){
if(this==&str) return str;
//发现自我赋值,直接返回原变量
//返回变量,以便出现连续赋值的情况
else{
delete[] m_data;
//如果自我赋值不写,这里杀死空间后,下一句就没法赋值了
//因为两个变量其实用的是同一个空间,杀死后就都消失了
m_data = new char[strlen(str.m_data)+1];
strcpy(m_data,str.m_data);
return *this;
//this是一个指针,我们需要返回变量
}
}
inline
String:: ~String(){
delete[] m_data; //new和delete的用法放到了后面介绍
}
//写成全局函数,是为了可以 cout << str 这样使用
//如果写成成员函数,在调用时是 str << 的用法,类似++
ostream& operator<<(ostream& os,const String& str){
os << str.get_c_str();
return os;
}
5.堆栈Stack、Heap
Stack是某个作用域内的一个内存空间,需要手动释放空间
m_data = new char[strlen(str.m_data)+1];
Heap由操作系统分配的空间
String s;
在离开作用域后就会被操作系统释放(函数内部的参数变量)
static String s;
静态变量在程序结束时才释放
全局变量:不是在某个函数内部定义的变量
全局变量在程序结束时才释放
内存是有限的,必须在使用后释放
new先分配空间,再调用构造函数
(分配指定大小的空间,将空间转化成指定类型,变量命名以及值的写入)
delete先调用析构函数,再释放空间
(调用析构函数删除类包含的基础变量或者类指针所指的值,再释放类变量空间除变量外的空间)
String:: ~String(){
delete[] m_data; //new和delete的用法放到了后面介绍
}
先删除了m_data,再释放String的空间
在分配变量空间时,不仅仅是变量占用的空间,还有空间头尾的字段(记录空间长度)、调试字段
这个总空间的长度必须是16字节的倍数,组成原理里有介绍为什么
new [ ]与delete[ ] 搭配
当new的空间存放的是指针时,delete就需要把在释放指针空间之前,把指针所指的变量空间也释放,这需要调用多次delete,就用delete[ ] 形式表示
当类定义中含有指针时,有许多地方需要注意
必须写深拷贝的构造和赋值函数,必须要写析构函数,必须在析构函数中使用delete[ ]
何时不可以使用by reference的形式:
当函数返回的值存放的空间是一个新的空间时,就只能用by value的形式
这个新的空间是在函数内部创建的,就需要存下来
如果返回值保存的空间在执行函数前就存在,就无需保存,直接把地址传出去即可
by reference本质上就是使用地址来传递信息
在函数内部创建的函数在,函数执行结束后就消失了
const 函数,是在函数不改变其包含的变量值的情况下才加
而且必须加
因为如果不加,当常量调用该函数时会报错
因为常量不会信任一个可能改变其变量值的函数
6.static 静态,唯一一份,但可以修改,不是常数
静态成员属于类,所有的类变量共用一个静态成员变量
实例成员属于类变量,每个类变量有自己的实例成员变量
静态变量必须要在类定义外部写一个初始化定义,那个时候才会分配内存
//只有静态变量才能这样写,换成普通变量就不行了
// 以银行账户类为例
class Account{
public:
static double m_rate;
static void set_rate(const double& x){
m_rate=x;///////////
// 普通的成员变量是不能这样写的
// 因为对于普通的成员变量,每个类变量都对应一个成员变量
// 所以应该在建立类变量之后,才能有
}
};
double Account::m_rate = 8.0;///////////
// 默认值
// 我不明白为什么静态变量不能直接在类里设置初始值
int main(){
Account::set_rate(5.0);/////////
// 在还没有建立类变量之前,就可以设置m_rate的值
Account a;
a.set_rate(7.0);
}
基于静态变量设计的单例模式
class A {
public:
static A& getInstance();
steup(){
...
}
private:
A();
A(const A& rhs);//构造函数放到了私有里,不允许外部创建类变量
...
}
A& A::getInstance(){
static A a;
return a;
}
// 把类定义的函数放到外面
// 只有在调用这个函数时才会真的去创建这唯一的一个类变量,更合理
// 如果把这个函数直接写到类里面,在创建类的时候就会创建这个唯一的类变量
// 把类定义的函数放到外面
// 只有在调用这个函数时才会真的去创建这唯一的一个类变量,更合理
// 如果把这个函数直接写到类里面,在创建类的时候就会创建这个唯一的类变量
7.类模板,通用的类
将类中涉及的变量类型换成变量,而不是固定的
在编译时会生成多份类结构
//一个通用型的类
template
//template是固定的,typename也是固定的,typename可以换成class,两者是等价的
class complex{
public:
complex(T r=0,T i=0) : re(r),im(i) {
};
complex& operator += (const complex&);
T real() const{//函数不会改变调用函数的类变量值
return re;
};
T imag() const{
return im;
};
private:
T re,im;
friend complex& _doapl (complex*,const complex&);
//友元函数可以直接使用类的私有成员函数和变量
}
int main(){
complex
complex
...
}
8.函数模板
class stone{
public:
stone(int w,int h,int we)
: _w(w),_h(h),_we(we)
{}
bool operator< (const stone& s) const {
//<是对左边的变量来调用的,就像b调用<函数,使用a参数代入
return _we<s._we;
}
private:
int _w,_h,_we;
};
template
inline
const T& min(const T& a,const T& b){
return b<a? b:a;
//<是对左边的变量来调用的,就像b调用<函数,使用a参数代入
}
stone r1(2,3),r2(3,3),r3;
r3=min(r1,r2);
9.namespace
加入命名空间,减少部分变量的表达代码(用来指明变量的来源类)
本质上namespace是一个类的代码
当我们写using *** 时,就是把这个类调用了,后面的代码都是在这个类的环境下编写的
比如说 using namespace std,就是调用了namespace std的代码
cin的全称应该是std::cin,但由于处于using namespace std环境下,就不必写std::
三种类和类之间的关系
10.复合 composition
类中的成员是另一个类
改造,适配
构造函数就是用来建立类对应的变量的
析构函数只有在包含指针的类中才有,因为在含指针的类中
类无法使用默认的浅拷贝,每个类变量的指针应该是不同的,例如两个类变量的Int类型值可以使用不同空间,存相同的值。但指针型成员不可以,不同的空间也不能存相同的值,必须再新建一个指针地址,这就是深拷贝。
这样带指针的类在用完后也就需要释放指针申请的新空间,也就是析构函数
构造,先基础后复合;析构先复合后基础
箭头表示基础类,黑色方块表示用基础类构造的复合类(容器)
(1)复合类的构造函数,会先执行基础类的默认构造,再执行自己的构造函数,由内到外
如果你需要的不是默认构造函数,就要自己写
(2)复合类的析构函数,先删去自己的,再删内部的,由外到内。因为在删除外部时可能会需要用到内部的函数,如果先释放了内部的基础类,那可能在释放外部类会出错
11.委托 delegation
类中的成员是另一个类的指针 composition by reference
区别在于是指针,图中表示就是白色的菱形框
将一个类的具体实现部分放到另一个类中,同时使用一个指针来指
这样可以在不改变左边的情况下,来修改右边的,非常灵活
因此在现代编程里非常常见
也被称为:point to implementation 指向实现的指针
string.h文件
class stringrep;
//因为后面的类定义函数里有其他类的指针,
//所以需要在这之前写一个其他类的定义函数
//因为真正需要执行的是.cpp文件,所以在头文件中不需要没有#include "main.cpp"的写法
class string{
public:
string();
string(const char* s);
string(const string& s);//拷贝构造
string &operator=(const string& s);//拷贝赋值
~string();//析构函数
private:
stringrep* rep;
};
main.cpp文件
include
include "string.h"
namespace {
class stringrep{
friend class string;//友元
stringrep(const char* ch);//构造函数
~stringrep;//含有指针必须有析构函数
int count;
char* rep;
};
};
12.继承
子类继承父类,子类是一种父类
父类的数据完整的继承到子类中
三角是父,线是子
构造和析构函数的写法与复合类似
父类的析构函数必须是virtual虚函数,否则会出现未定义行为
struct node_base{
node_base* next;
node_base* prev;
}
template
struct node
:public node_base{//这里就是继承的写法
T data;
}
13.虚函数
非虚函数,不允许子类重新定义
虚函数,允许
纯虚函数,子类必须要重新定义
空函数不是纯虚函数,只有写成const=0才是
子类可以直接使用父类的变量和函数,但也可以重新定义,不定义就是用父类的
最完美经典的虚函数执行流程(灰线)
template method
经典设计模式一共有23个
父类就是应用框架,子类是对父类基础操作的组合和专业化
继承与复合的组合
多态:
继承与委托的复合(效果最好的)
对于同一份数据,具有多种表现形式;数据改变,则所有表现形式都会改变
左边的subject就是数据,他具有多个指针
子类创建的变量也是一种特殊的父类变量,所以子类变量可以放到数据类subject的容器vector中
资料observer1的变量ob也属于observer父类,同时ob可以使用observer1的独有函数
observer1 ob;
//资料observer1创建的变量ob也属于observer父类,同时ob可以使用observer1的独有函数
一份数据生成三种图
代码里的vector表示动态数组容器,可以根据需要自动调节长度
14.委托相关设计
文件系统:
primitive 个体文件
componet 文件
composite 复合文件
个体文件和复合文件都是一种文件,所以文件是他们的父类
同时复合文件包含多个文件,使用委托,每个文件都可能是个体文件或者复合文件
复合设计模式,真的非常漂亮美丽
先画图后写类,真的非常棒
文件的add不能写纯虚函数,因为个体文件没有add
原型设计模式
父类不知道子类的名称
子类该怎么将自己放入到父类的容器里呢?
解法是:在创建子类landSatImage时,先创建一个静态的子类变量LSAT(子类的一个成员是静态的子类自身),创建时就会调用子类的构造函数landSatImage(),该构造函数会调用addPrototype函数,这是父类的函数,他会将创建的类变量放进父类的容器里。
这样就实现了在父类不知道子类名字的情况下,将子类变量放入父类容器
当我们真的需要创建一个子类变量,并且不想放入父类容器时;就需要写一条不同的构造函数,不放入父类,加一个参数来区别(不加参数的构造函数会将变量放入父类容器,加参数的不会,本质上这个参数不使用,仅用来区分两个构造函数)
图示里LSAT的下划线表示静态
-表示private
表示protected
不写就是public
父类
子类
标签:上期,const,函数,C++,String,complex,侯捷,data,变量 From: https://www.cnblogs.com/atopes-chw/p/18202129