非常浅的的涉及了一下C++ markdown语法有些地方和我用的bear上不一样看着非常难受 请见谅
面向对象的程序设计的特点:Encapsulation 、Inheritance 、Polymorphism
概述
- 如何形成生成程序
- 编辑(.cpp .h)
- 编译(.obj)预处理 # include 和# define 等等以及逐语句处理分析语法、合成
- 连接(link .obj.lib)形成exe
- 运行
- C++是一种不存粹的OOP,C#是一种纯粹的OOP
- 区分interpreter和compiler 了解分离式编译
- interpreter是将源代码在程序运行的时候逐行形成活动(activities),这样导致程序在运行的时候非常慢
- compiler是将源代码翻译成汇编或者是机器代码,这样程序在运行的时候非常快并且占用的空间少,但是在编译的时候会慢,有些语言甚至还支持分离式编译
- 区分:declaration是告诉编译器有这么一个变量,definition为这个变量申请存储空间
- extern只是一个申明,定义这个变量在外部文件里
- fstream示例
#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <fstream>
using namespace std;
int main()
{
ifstream in("file1.txt");
ofstream out("file2.txt");
string s;
while(getline(in,s)){
cout << s << endl;
}
cout << "End" << endl;
out.close();
in.close();
return 0;
}
C与C++
enum bool = {false = 0,true = 1};
可以直接在c语言中定义出bool类型
?: ternary conditional expression三元运算符
sizeof()也是运算符
- 基本/内置类型(basic types/built-in types) char float bool int void
- void类型是不能定义变量的,用于函数的返回类型,定义void类型的指针(编译器总会分配4个bit的数据空间,和指针指向的对象没有任何关系)
- 变量的传递使用过传递地址、值传递、引用传递
- 引用如果在类中一定要在类的初始化列表中初始化,或者是在申明的时候初始化
- 变量的作用域是包含他且离他最近的左 { 以及与其匹配的右 }
- 全局变量和静态变量在heap中申请(函数即使结束静态变量还是存在的 因为函数是在stack中的)
- volatile可以告诉编译器可能改变变量的值。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
- 左值是一个变量,右值是一个常量或者是能产生一个值的变量和表达式
- x = x + 4; 和 x += 4;编译上存在一定的区别 重载运算符的时候也有一定的区别
- 使用sizeof可以使得程序具有可移植性
- 强制类型转换
double slope = static_cast<double>(j) / i;
只要不包含底层const就可以进行static_cast转换。还可以找回void *指针
void *p = &d;
double *dp = static_cast<double *>(p);
const_cast用于改变运算对象底层的const性质 (比如指针指向的对象)
9. struct中的内存分配是使用内存对齐的(和内存最大的变量分配一样多的内存)以方便编译器使用偏移量(默认是4个字节)来计算占用的内存大小
class Node{
int num;
char a[5];
};// 8 < 4+5 <= 12,像char是一个字节就可以一个一个分配但是int不行
以上class占据12个字节
10. enum枚举类型(如果不给枚举类型赋值,编译器从0开始赋值或者是顺序增加)
11. 函数在编译并且被载入到计算机中执行,就占据了chunk of memory(一块内存),所以函数也可以用指针去调用,如:void (*Funptr)();
抽象数据
- 静态内存分配:在编译(compile)的时候分配内存
而动态内存分配是在运行(run)的时候分配内存 - new和delete用于动态内存分配
int a;
int *p = new int (0);
int *k = new int[256*1024]; //1M内存
if(p != nullptr)
delete p; //删除的是申请的p指向的内容,但是p本身还是存在的 假如p指向其他内存,则原来的地址已经泄漏
if(k != nullptr)
delete[] k;
else
cout << "Memory exhaustion." << endl;//假如说多次申请之后k=nullptr说明内存耗尽
p = &a;
*p = 0;
cout << (*p) <<endl;
- ⚠️其实使用void*仍然可以访问或者更改CStach中的内容,所以最好的解决方案就是把void *更改成const void*fetch()或者说在添加一个元素,将index的值赋给这个元素,将元素的地址返回出去
- 在类的函数进行类类型参数传递时,可以考虑使用引用,这样不会使用更多的内存空间
- 构造函数:只要有新对象生成就要调用构造函数;和类具有相同的名字;自动被C++调用;被调用来创建一个类对象;没有返回类型;不能被对象调用;如果你没有定义构造函数,C++会给你自动创建一个构造函数,就相同于自己定义的
Classname(){};
默认类的构造函数
- 构造函数可以通过参数的类型和数量的不同进行重载
- 构造函数成员初始化顺序和类定义中的出现顺序一致
- 析构函数同理(但是析构函数不能重载:因为没有参数;同时析构函数可以被对象调用)
- 析构函数:
~xxx(){}
无参数,没有返回类型(可以有函数体);在对象被释放的时候自动调用,先构造的对象最后被析构,最后被构造的函数最先被析构,构造函数是唯一的。 - 对象的地址就是第一个成员变量的地址
- friend 可以访问类的私有部分
- 一个friend全局函数作为一个类的友元
- 一个friend全局函数作为两个类的友元
- 也可以把一个类的成员函数申明为另一个类的友元
- friend class友元类
函数重载
- (函数多态性的体现)参数个数不同或者参数类型不同就可以区分函数重载 ⚠️参数个数相同并且参数类型相同只有返回类型不同不能进行函数重载;
- 函数不仅是由名字来进行区分,同时也从参数的类型,数量,const限定词(顶层不起作用)进行区分,这个叫做签名。(编译器在编译时其实将重载的函数名变成了“函数名具体名字”)
- 带缺省值参数/默认参数的函数
const
区分const的顶层性质和底层性质
const int N = 1000+10;
const char * p; //底层const性质描述的是指向的对象
char * const p; //顶层const性质描述的是自己
const char * const p//指向常量的常量指针
const char a[N] = {...}//常量数组,底层性质
- 指向非常量的指针不能指向常量,指向常量的指针可以指向非常量对象
static
- 函数由于放在栈中,用完函数就释放空间了,然而static内存分配在堆中,只有在进程结束时才清空static
- 静态成员必须初始化,并且不能在构造函数中初始化(如果像这样静态成员将反复初始化,同时意味着必须在全局中初始化)
- 没有this指针,不能操作类的成员
- 静态成员函数或者是成员变量都可以使用作用域运算符::来调用,也可以使用点运算符.进行调用
常量再探
- 常成员函数(const member functions)
type functionname(arguments)const
成员函数不修改成员变量,常成员函数的const可以进行函数重载(const意味着this指针是常量)
类外定义常成员函数const 后缀(suffix)一定不能省略 - 常量对象(const objects):意味着常对象中的成员是不能改变的,同时只能使用常量对象中的常成员函数(不能调用普通的成员函数)
- 常量成员(const data member):const int a或者是常量引用成员const int &a一定要在构造函数的初始化列表中初始化
mutable
- 告诉编译器const成员变量也许会改变(用来强行改变常成员函数的性质)
内联函数(inline函数)
- 宏定义由于会产生难以发现的bug,同时不能作为类的成员函数所以我们在C++中引入了inline(内联函数)
- 减少内存,CPU开销
- ⚠️不能抛出异常;内联函数无法递归;调用内联函数时必须先声明;一般来说小的函数可以定义成inline函数。inline函数是一种用空间换取时间的做法
- 当你在类中定义成员函数,类自动将其隐式转变成inline函数
#include <iostream>
using namespace std;
class Member {
int i;
public:
Member(int x = 0) : i(x) { cout << "Member! i = " << i << endl; }
~Member() { cout << "~Member i = " << i << endl; }
};
class WithMembers {
Member q, r, s; // Have constructors?
int i;
public:
WithMembers(int a, int b) : s(a), q(b) { i = a;}
~WithMembers() { cout << "~WithMembers" << endl; }
};
int main() {
WithMembers wm(3, 9);
return 0;
}
//程序输出
/*
Member! i = 9
Member! i = 0
Member! i = 3
~WithMembers
~Member i = 3
~Member i = 0
~Member i = 9*/
- 构造函数的调用不是由列表决定,而是由类中成员的定义顺序决定,同样析构函数的调用就像出栈一样调用析构函数withmember这样明确要出输出的东西是要先调用的
域名
class xxx
//some other members
}
⚠️;不是必要的,可以用别名namespace fsp = Myspace,放在全局声明中
1. 使用方式Myspace::xxx.set(),或者在上边写using namespace Myspace;
2. 当使用出现冲突,即使申明了using….还是要在具体调用的对象之前加上Myspace::….,全局函数::….
引用类型和拷贝构造函数
- argument一般指的是实参,parameter一般指的是形参
- 使用引用或者是指针一定要注意对象的作用域(比如说函数返回类型是指针或者是引用,那么一定不能返回局部参数的指针或者是引用)
- 编译器会自动给你定义一个默认的拷贝构造函数
- 拷贝构造函数是一种构造函数,参数列表是这一种类的引用对象
- 在函数传参时调用
- 初始化一个对象的时候
- 当函数返回类类型,主动调用拷贝构造函数 将返回类赋值给匿名对象(anonymous object 是个全局变量)匿名对象将值传给真正的赋值对象
- ⚠️定义拷贝调用函数一定要使用引用,如果不是传参的时候赋值使用拷贝函数
- 指向成员的指针 注意只能指向public部分成员(当然可以定义指向函数指针);
使用方式有两种:定义一个指向对象的指针,用对象的指针->调用指向成员的指针或者是使用作用域运算符.调用指向成员的指针
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
class Location {
public :
Location (int xx=0,int yy=0){
X=xx ; Y=yy; cout << "Object constructed." << endl ;
}
Location ( const Location & p ) ;
~Location () { cout << X << "," << Y << " Object destroyed." << endl ; }
int GetX () { return X; }
int GetY () { return Y; }
void print()const{
cout << X <<" "<<Y << endl;
}
//private:
int X,Y;
};
Location :: Location ( const Location & p ){
X= p.X;
Y= p.Y;
cout << "Copy_constructor called." << endl;
}
Location g () {
Location A(1,2);
return A;
}
int main () {
Location B(1,2),*Bp = &B;
int Location::*pBcont = &Location::X;
Bp->*pBcont = 4;
pBcont = &Location::Y;
B.*pBcont = 9;
// B.print();
int (Location::*pmem)() = &Location::GetX;
cout << (B.*pmem)() << endl;
pmem = &Location::GetY;
cout << (Bp->*pmem)() << endl;
return 0;
}
运算符重载
- 基本的运算类型的运算符是不能重载的,只有用户定义的类型的运算符才能进行重载(只是一种syntactic sugar语法糖衣,让程序方便阅读)
type operator @(argument list){
}
classname operator --(int){} //x++
classname operator ++(){} //++x
- 一元运算符(只能重载成类的成员函数)、二元运算符可以重载,6种三元运算符不能重载,分别是:
. //成员选择运算符
:: //作用与选择运算符
?: //三目运算符
.* //指针的成员选择运算符
sizeof() 以及 typeid()
- ++参数列表中无参数表示++x;有参数表示x++(此时参数是个哑元(dummy)表示++是一个postfix后缀);
a.operator = (n.operator++(int));
b.operator = (n.operator++());
- PPT12张,p2 = p1,两者的指针指向同一块内存,这样delete会释放一个内存两次,所以需要重载运算符。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
class pointer{
private:
int *p;
public:
pointer(int x){
p = new int (x);
};
~pointer(){
if(p != nullptr)
delete p;
};
pointer& operator = (const pointer&);
void Display(){
if(p != nullptr)
cout << *p << endl;
else
cerr << "NULL" << endl;
}
};
pointer& pointer::operator = (const pointer& T){
if(this != &T) //防止自己给自己赋值
*p = *T.p;
return *this;
}
int main(){
pointer x(10),y(20);
x = y;
y.Display();
x.Display();
return 0;
}
- 构造函数 析构函数 拷贝构造函数 赋值重载函数在一个类中是一定存在的
- 13张z = x + 3等同于 z = (x.operator+(3))调用构造函数看看能不能将3构造成complex类型。但是这样无法运算z = 3 + x(因为3.operator+(x)不能调用)
所以像这种二元运算的运算符重载的时候应该定义一个友元函数 - x+=3比x = x + 3好,+=调用的函数少,开销的内存更少(尤其涉及类的运算的时候)
- 转换运算符
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
class A{};
class Rational{
friend ostream&operator <<(ostream&,const Rational&);
public:
explicit Rational(double num = 0,double den = 1){ Numerator = num, Denominator = den;}
Rational(const Rational&);
~Rational(){};
Rational& operator = (const Rational &);
operator double(){return Numerator/Denominator;}//类型转换操作符
// operator A(){cout << "Rational to A" << endl;}
private:
double Numerator,Denominator; //分子分母
};
Rational& Rational::operator=(const Rational &obj){
Numerator = obj.Numerator;
Denominator = obj.Denominator;
return *this;
}
Rational::Rational(const Rational& T){
Numerator = T.Numerator;
Denominator = T.Denominator;
}
ostream& operator <<(ostream &os,const Rational& obj){
os << "This is 分子:" << obj.Numerator << endl;
os << "This is 分母:" << obj.Denominator << endl;
return os;
}
class vec{
public:
explicit vec(int size){
if(size > 0){
v = new int[size];
Size = static_cast<unsigned int>(size);
}
}
~vec(){if(v!=nullptr)delete[]v;}
int& operator[](int Index){
if(Index < 0||Index >= Size){
cerr << "Invalid Index." <<endl;
exit(0);
}
else
return v[Index];
}
private:
int *v;
unsigned int Size;
};
inline void test_Rational(){
Rational a(100,200),b;
Rational c(100.0); //必须显式调用构造函数
b = a;
double d = c;
cout << d << endl;
//A aa;
//aa = b;
cout << a;
}
inline void test_vec(){
vec tvec(10);
tvec[3] = 10;
cout << tvec[3] << endl;
}
int main(){
test_Rational();
// test_vec();
return 0;
}
- explicit用于表明函数不允许构造函数隐式转换,要求必须被显式调用
- 重载下标运算符[]
- 重载括号运算符
- 重载<<运算符
动态对象的建立
- 重载new delete可以是全局和成员函数
- 重载new delete中不能使用cin cout只能使用printf和puts
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
const int size = 10;
class Tree{
int height;
public:
Tree(){}
Tree(int treeH):height(treeH){}
~Tree(){cout << "Destroyed" <<endl;}
friend ostream& operator << (ostream &os,const Tree *obj){
os << obj->height << endl; //指针一定要用->
return os;
}
};
int main(){
Tree *t = new Tree(5);
cout << t;
delete t;
Tree *arr_t = new Tree[size];
delete [] arr_t;
return 0;
}
- 如果说new的内存不够,那么就会去调用一个new_handler函数
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstdlib>
#include <new.h>
#include <cstring>
using namespace std;
const int size = 10;
int counter = 0;
int out_of_memory(size_t){
cerr <<"mem exhausted after" <<counter << "allocations!";
exit(1);
}
int main(){
_set_new_handler(out_of_memory);
while(1){
++counter;
new int[10000000000*10000000000];
}
return 0;
}
- 重载new的例子
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <new>
using namespace std;
const int maxn = 100;
void *operator new(size_t sz){
printf("operator new: %lu Bytes\n", sz);
void *m = malloc(sz);
if(m == nullptr)
puts("Out of mem.");
return m;
}
void operator delete(void *m){
puts("operator delete");
free(m);
}
class S{
private:
int i[maxn];
public:
S(){puts("S::S()");}
~S(){puts("S::~S()");}
};
int main(){
int *p = new int (47);
delete p;
puts("\n");
S *s = new S;
delete s;
puts("\n");
S *sarr = new S[3];
delete []sarr;
return 0;
}
继承(Inheritance)和组合(composition)
- 不管是继承还是组合都要在后来的类中初始化继承来的或者组合的类
- 组合是一种类的嵌套
- 继承则是由基类(base class也叫super class/father class/ancestor class)和派生类(derived class也叫subclass/posterity)组成,目的是用派生类继承基类
- 除了构造函数和析构函数和重载的赋值运算符是不被继承的(因为看上去就像一个构造函数),其他都是被继承的;关于构造函数:derived class会隐式(implicit)调用默认构造函数,假如说base class中的构造函数都是有参数的,那么要在derived class中的初始化列表显式调用构造函数。
- 分类单继承和多继承
- 继承时
class manager:public employee{} //public指继承的权限
- ⚠️公有继承不破坏基类的访问权限,但是派生类无论继承权限是什么,派生类的对象或者是其本身的成员函数都无法访问基类的private部分
- 派生类manager的可以访问基类employee的public部分
- 派生类的成员函数可以访问基类的protected和public部分的成员函数和成员数据,但是派生类的对象是不能访问protected部分的
protected的作用就是类的对象是不能访问的,但是可以让派生类的成员函数访问
- 继承时
class manager:private employee{
} //private将employee的公有成员和保护成员变成private,这样派生类的子类(派生类)无法访问
class manager:protected employee{
} //将employee的公有成员和保护成员变成protected,这样只能由派生类的成员函数和友元访问
下面列出三种不同的继承方式的基类特性和派生类特性。
public protected private
公有继承 public protected 不可见
私有继承 private private 不可见
保护继承 protected protected 不可见
上边:1)基类成员对派生类都是:共有和保护的成员是可见的,私有的的成员是不可见的。
2)基类成员对派生类的对象来说:要看基类的成员在派生类中变成了什么类型的成员。如:私有继承时,基类的共有成员和私有成员都变成了派生类中的私有成员,因此对于派生类中的对象来说基类的共有成员和私有成员就是不可见的。
5. 调用构造函数:先初始化基类,然后初始化成员类,最后调用自己的构造函数 ;析构函数调用顺序刚好相反
6. 要防止基类和派生类的函数名相同,派生类的函数会覆盖,无法调用 例:employee和manager都定义了print一定想调用employee中的print:manager M;
M.employee::print();
7. upcasting 派生类总是可以upcasting成基类,并且总是安全的
8. 多继承中的问题
* 在多继承中基类有相同函数名,这样在类对象调用函数时会产生ambiguous,解决方案:指明
class C:public A,public B{};
int main() {
C obj;
//obj.fun();
obj.A::fun(); //call A’ fun()
obj.B::fun(); //call B’ fun()
return 0;
}
或者是在定义派生类C的时候定义新的函数
class C : public A, public B
{
public:
void fun() //Name Hiding
{ A::fun(); B::fun(); }
};
⚠️两个基类中的fun函数仅仅通过函数变量的类型不同是不能区分的
* 一个派生类继承了继承了两个基类,这两个基类继承了同一个基类
class A {
public:
void fun();
};
class B : public A { //virtual public A
public:
void FB();
};
class C : public A { //virtual public A
public:
void FC();
};
class D : public B, public C { };
void main() {
D obj;
obj.FB(); //ok
obj.FC(); //ok
obj.fun(); //ambiguous 使用虚继承可以解决这个问题
}
这种菱形继承产生问题:解决方案使用虚继承
- 虚继承
虚基类的构造函数(初始化)由最派生类(比如说上边的D)去完成,但构造函数的调用次序不同。派生类构造函数的调用次序有三个原则:
- 虚基类的构造函数在非虚基类之前调用;
- 若同一层次中包含多个虚基类,这些虚基类的构造函数按它们说明的次序调用;
- 若虚基类由非虚基类派生而来,则仍先调用基类构造函数,再调用派生类的构造函数。
- 继承应该是描述的越来越小,而组合(也就是包含了小类)相当于小类是一个组成部分
函数多态性和虚函数
- 是否需要将函数定义成虚函数应该考虑派生类中的函数是否需要继续使用这种函数
- 在类中有虚函数表,使用指针去访问
- 定义虚函数的时候(dynamic/runtime/late binding动态绑定)否则称为是(early/static binding静态绑定)
- 一定要有相同的返回类型,相同的函数名称,相同的形参类型,形参顺序,形参数量
- 虚函数一定要是类的成员函数,友元、全局函数定义成虚函数是没有意义的
- 构造函数不能定义成虚函数,析构函数可以定义成虚函数
-
- 纯虚函数(pure virtual function)
virtual void show() = 0;//仅仅告诉编译器有一个定义没有实现,是纯虚函数
纯虚函数一般不给出代码,但是有时比如像析构函数,会在基类的虚析构函数中给出或者是一段代码,派生类中的函数都会用到。 - 抽象类(abstract class):含有纯虚函数的类叫做抽象类,不能定义对象(个人觉得这是一个最底层的模板),仅仅只能作为其他类的基类,而抽象类的派生类用来示例对象
- 纯虚析构函数
- 纯虚函数(pure virtual function)
#include <iostream>
using namespace std;
class Pet{
public:
virtual ~Pet() = 0;
};
// Don’t implement in the class
Pet::~Pet() {cout << "~Pet()" << endl; }
class Dog: public Pet{
public:
~Dog() { cout << "~Dog()" << endl; }
};
int main(){
Pet *p = new Dog();
// Virtual destructor call
delete p;
return 0;
}
//~Dog()
//~Pet()
- downcasting
C++中定义了dynamic_cast,用来将转换成指定的类型,如果转换合适并且成功,那么会返回一个指针,如果不成功,返回空值。
#include <iostream>
using namespace std;
class Pet{ public: virtual ~Pet() { } };
class Dog: public Pet{ };
class Cat: public Pet{ };
int main()
{
Pet *b = new Cat(); // Upcase
// Try to cast it to Dog*
Dog* d1 = dynamic_cast<Dog*>(b);
// Tru to cast it to Cat*
Cat* d2 = dynamic_cast<Cat*>(b);
cout << "d1 = " << (long)d1 << endl;//空值
cout << "d2 = " << (long)d2 << endl;//有地址的
delete b; // call base destructor automatically
return 0;
}
类模板
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
template <class T>
T abs(T x){
return x >= 0? x:-x;
}
int main(){
cout << abs(8.1) << endl;
return 0;
}
一些其他的东西
- C++中所有的指针都是占据一样的内存空间的,但是指向的对象的大小是不一样的
- 抽象类(abstract class)虽然不能定义对象(即不能实例化),但是可以定义抽象类的指针
- 注意一些陷阱
int x,y = 14,z =5;
x = static_cast<double>(y)/z;
这个时候x输出还是2
4. 基类不能向派生类转换,这样是填不满的(编译器报错),但是我们可以通过dynamic_cast将指向基类的指针转换成指向派生类的指针(这样做实际上是不安全的,但是编译器不报错,也不会产生warning)
5. 继承的时候构造函数,析构函数,友元函数,重载的赋值运算符函数是不能被继承的
6. 静态成员函数可以重载,静态成员变量不能被构造函数初始化!
7. 类的多态性(polymorphism)的体现是
* 编译器的多态:重载和模板
* 运行期的多态:虚函数
8. 纯虚函数相当于一个函数的接口,所以不能被继承
9. 常成员变量一定要在构造函数的初始化列表中初始化!(类中如果有引用一定要在申明的时候初始化,或者像常成员变量一样列表初始化)常成员对象只能访问常成员函数
10. 如果不定义类的不含参数的构造函数(C++这个时候就不给你定义默认构造函数了),那么申明变量的时候classname obj就会出错
11. class如果说明继承方式默认private继承,struct默认public继承