c++知识点总结3
阅读原文时间:2023年07月15日阅读:3

http://akaedu.github.io/book/


week1

引用:相当于变量的别名。下面r和n就相当于同一回事

int n=;
int &r=n;

引用做函数参数:

void swap(int &a, int &b){ //a等价于n1, b等价于n2
int tmp;
tmp=a; a=b; b=tmp;
}

int n1, n2;
swap(n1, n2);

const int &r=n;        //不能通过常引用r来修改n的内容。

const

const int MAXAL=;
const char* NAME="pentium";
int n;
const int* p=&n; //不能通过常量指针p来修改n的内容
int* q=p; //不能将常量指针赋给非常量指针,反之可以

void func(const char* p){
….
不能提供常量指针p修改内容
}

动态内存分配

int *pn=new int;
*pn=;
//动态分配出sizeof(int)这么大的内存,并将其起始地址返回给pn

int *pm=new int[];
pm[]=;
//动态分配数组
//new出来的对象必须手动delete。否则即使运行结束了系统也不会自行销毁

delete pn;
delete[] pm;

inline:内联函数

将函数体代码直接解开插入原文,避免了调用函数的开销。适用于短小常用的函数

函数重载:名字相同但参数表不同


week2

面向对象:抽象、封装、继承、多态

访问范围:public / protected / private

构造函数:对象初始化时执行。可以被重载。当重载了多个构造函数时,默认使用无参数构造函数初始化。

构造函数有两种特殊类型:

拷贝构造函数:形如 ClassName::ClassName(ClassName& obj)   或   ClassName::ClassName(const ClassName& obj)。

作用:复制一个对象。

对象被初始化时会被调用。若函数的形参/返回值是一个对象,它的复制构造函数也会被调用

ClassName c2(c1);     ClassName c2=c1;    //初始化语句,都会调用拷贝构造函数

编译器会默认配一个复制构造函数。

拷贝构造函数的参数必须是引用,不能是ClassName obj

ref:https://blog.csdn.net/lwbeyond/article/details/6202256

类型转换构造函数:Class1::Class1(Class2& obj)

析构函数:ClassName::~ClassName()

对象消亡时会被调用


week3

this指针:指向成员函数所作用的对象。非静态成员函数中可以直接使用this来代表指向该函数作用的对象的指针

class Complex {
public:
double real, imag;
void Print() { cout << real << "," << imag ; } Complex(double r,double i):real(r),imag(i) { } Complex AddOne() { //等价于Complex AddOne(Complex* this) this->real ++; //等价于 real ++;
this->Print(); //等价于 Print
return * this;
}
};

int main() {
Complex c1(,),c2(,);
c2 = c1.AddOne(); //this指向对象c1,所以返回的就是c1
//等价于c2=AddOne(c1);
return ;
} //输出 2,1

静态成员函数/静态成员变量:整个类共有的,不属于某个对象。

静态成员函数并不具体作用与某个对象,所以静态成员函数中没有 this 指针

class CRectangle
{
private:
int w, h;
static int nTotalArea; //静态成员变量
static int nTotalNumber;
public:
CRectangle(int w_,int h_);
~CRectangle();
static void PrintTotal(); //静态成员函数
};

普通成员变量每个对象有各自的一份,而静态成员变量一共就一份, 为所有对象共享。

普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用于某个对象。

因此静态成员不需要通过对象就能访问。

类名::成员名
CRectangle::PrintTotal();

对象名.成员名
CRectangle r;
r.PrintTotal();

指针->成员名
CRectangle * p = &r;
p->PrintTotal();

引用.成员名
CRectangle & ref = r;
int n = ref.nTotalNumber;

静态成员变量本质上是全局变量,哪怕一个对象都不存在,类的静态成员变量也存在。
        静态成员函数本质上是全局函数。
        设置静态成员这种机制的目的是将和某些类紧密相关的全局变量和函数写到类里面,看上去像一个整体,易于维护和理解。

在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数

封闭类:有成员对象(其他类的对象,和int这种成员变量不同)的类

封闭类的构造函数需要声明初始化列表。

class CTyre {
public:
CTyre() { cout << "CTyre contructor" << endl; }
~CTyre() { cout << "CTyre destructor" << endl; }
};

class CEngine {
public:
CEngine() { cout << "CEngine contructor" << endl; }
~CEngine() { cout << "CEngine destructor" << endl; }
};

class CCar {
private:
CEngine engine;
CTyre tyre;
public:
CCar( ) { cout << “CCar contructor” << endl; }
~CCar() { cout << "CCar destructor" << endl; }
};

int main(){
CCar car;
return ;
}

输出结果:
CEngine contructor
CTyre contructor
CCar contructor
CCar destructor
CTyre destructor
CEngine destructor

封闭类对象生成时,先执行所有对象成员的构造函数,然后才执行封闭类的构造函数。当封闭类的对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数。最先生成的最后析构。

常量对象&&常量成员函数

const Sample Obj; // 常量对象。常量对象只能使用构造函数、析构函数和 有const 说明的函数(常量成员函数)

在类的成员函数说明后面可以加const关键字,则该成员函数成为常量成员函数。常量成员函数内部不能改变属性的值,也不能调用非常量成员函数

class Sample {
private :
int value;
public:
void func() { };
Sample() { }
void SetValue() const {
value = ; // wrong
func(); //wrong
}
};
const Sample Obj;
Obj.SetValue (); //常量对象上可以使用常量成员函数
void Print(const Sample & o) {
o.PrintValue(); //若 PrintValue非const则编译错
}

如果一个成员函数中没有调用非常量成员函数,也没有修改成员变量的值,那么, 最好将其写成常量成员函数。

常量成员函数/常量成员变量可以重载:定义一个const的,一个非const的

mutable成员变量:可以在const成员函数中修改的成员变量

class CTest{
public:
bool GetData() const{
m_n1++;
return m_b2;
}
private:
mutable int m_n1;
bool m_b2;
};

友元

友元函数:一个类的友元函数可以访问该类的私有成员

友元类:一个类的成员函数可以是另一个类的友元

A是B的友元类->A的成员函数可以访问B的私有成员

友元类之间的关系不能传递,不能继承

class CCar ; //提前声明 CCar类,以便后面的CDriver类使用
class CDriver
{
public:
void ModifyCar( CCar * pCar) ; //改装汽车
};
class CCar
{
private:
int price;
friend int MostExpensiveCar( CCar cars[], int total); //声明该类的友元函数
friend void CDriver::ModifyCar(CCar * pCar); //声明该类的友元函数。可以将一个类的成员函数(包括构造、析构函数)说明为另一个类的友元
};
void CDriver::ModifyCar( CCar * pCar)
{
pCar->price += ; //友元函数可以访问private
}
int MostExpensiveCar( CCar cars[],int total)//友元函数可以访问private
{
int tmpMax = -;
for( int i = ;i < total; ++i ) if( cars[i].price > tmpMax)
tmpMax = cars[i].price;
return tmpMax;
}

class CCar{
private:
int price;
friend class CDriver; //声明CDriver为友元类
};
class CDriver{
public:
CCar myCar;
void ModifyCar() {
myCar.price += ;//因CDriver是CCar的友元类,故此处可以访问其私有成员
}
};


week4

运算符重载

class Complex{
public:
double real,imag;
Complex( double r = 0.0, double i= 0.0 ):real(r),imag(i) { }
Complex operator-(const Complex & c);
};
Complex operator+( const Complex & a, const Complex & b){
//重载为普通函数
return Complex( a.real+b.real,a.imag+b.imag); //返回一个临时对象
}
Complex Complex::operator-(const Complex & c){
//重载为类的成员函数
return Complex(real - c.real, imag - c.imag); //返回一个临时对象
}

int main(){
Complex a(,),b(,),c;
c = a + b; //等价于c=operator_+(a,b);
cout << c.real << "," << c.imag << endl;
cout << (a-b).real << (a-b).imag << endl; //a-b等价于a.operator_-(b)
return ;
}

赋值运算符重载

有时候希望赋值运算符两边的类型可以不匹配,比如,把一个int类型变量赋值给一个Complex对象,或把一个 char * 类型的字符串赋值给一个字符串对象,此时就需要重载赋值运算符“ =”。赋值运算符“ =”只能重载为成员函数

class String {
private:
char * str;
public:
String ():str(new char[]) { str[] = ;}
const char * c_str() { return str; };
String & operator = (const char * s);
String::~String( ) { delete [] str; }
};
String & String::operator = (const char * s){ //重载“=”使得 obj = “hello”能成立
if( this == & s) return * this; //如果写成String s; s=s; ,一开始就把s给delete了。要防止这种情况
delete [] str;
str = new char[strlen(s)+];
strcpy( str, s);
return * this;
}

int main(){
String s;
s = "Good Luck," ; //等价于 s.operator=("Good Luck,");
cout << s.c_str() << endl;
// String s2 = "hello!"; //这条语句会出错

s = "Shenzhou 8!";               //等价于 s.operator=("Shenzhou 8!");  
cout << s.c\_str() << endl;  
return ;  

}

输出:
Good Luck,
Shenzhou !

Operator = 的返回值:ClassName &  (ClassName的引用)

注意String的浅拷贝和深拷贝:如果是String原生的等于号:

String S1,S2;
S1="aaaa";
S2="bbbb";
S1=S2 //S1会指向S2的内存空间

如不定义自己的赋值运算符,那么S1=S2实际上导致 S1.str和 S2.str指向同一地方。
如果S1对象消亡,析构函数将释放 S1.str指向的空间,则S2消亡时还要释放一次,不妥
另外,如果执行 S1 = "other";会导致S2.str指向的地方被delete
因此要在 class String里添加成员函数。

String的默认复制构造函数也有类似的问题

一般情况下,将运算符重载为类的成员函数,是较好的选择。但有时,重载为成员函数不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元

class Complex
{
double real,imag;
public:
Complex( double r, double i):real(r),imag(i){ };
Complex operator+( double r );
friend Complex operator + (double r,const Complex & c);
};

运算符重载实例:可变长整型数组 (略)

流运算符的重载(略)

重载类型转换运算符

#include
using namespace std;
class Complex{
double real,imag;
public:
Complex(double r=,double i=):real(r),imag(i) { };
operator double () { return real; } //重载强制类型转换运算符double
};
int main(){
Complex c(1.2,3.4);
cout << (double)c << endl; //输出 1.2
double n = + c; //等价于 double n=2+c.operator double()
cout << n; //输出 3.2
}

自增,自减运算符的重载(略)


week5

继承/派生(同一个概念)

子类(派生类)的成员函数不能访问父类的private

class CStudent {
private:
string sName;
int nAge;
public:
bool IsThreeGood() { };
void SetName( const string & name )
{ sName = name; }
//……
};
class CUndergraduateStudent: public CStudent {
private:
int nDepartment;
public:
bool IsThreeGood() { …… }; //覆盖
bool CanBaoYan() { …. };
};

// 派生类的写法是: 类名: public 基类名

在派生类对象中,包含着基类对象,而且基类对象的存储位置位于派生类对象新增的成员变量之前

派生类可以定义一个和基类成员同名的成员,这叫覆盖。

在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员。

要在派生类中访问由基类定义的同名成员时,要使用作用域符号::

基类的private成员:可以被下列函数访问
– 基类的成员函数
– 基类的友元函数

基类的public成员:可以被下列函数访问
– 基类的成员函数
– 基类的友元函数
– 子类的成员函数
– 子类的友元函数
– 其他的函数

基类的protected成员:可以被下列函数访问
– 基类的成员函数
– 基类的友元函数
– 子类的成员函数可以访问函数所在对象的父类的protected变量(别的对象不可以)

派生类的构造函数

子类的构造函数如何初始化基类的private变量?用初始化列表

class Bug {
private :
int nLegs; int nColor;
public:
int nType;
Bug ( int legs, int color);
void PrintBug (){ };
};
class FlyBug: public Bug{ // FlyBug是Bug的派生类
int nWings;
public:
FlyBug( int legs,int color, int wings);
};
Bug::Bug( int legs, int color){
nLegs = legs;
nColor = color;
}

//错误的FlyBug构造函数
FlyBug::FlyBug ( int legs,int color, int wings){
nLegs = legs; // 不能访问
nColor = color; // 不能访问
nType = ; // ok
nWings = wings;
}

//正确的FlyBug构造函数:
FlyBug::FlyBug ( int legs, int color, int wings) : Bug(legs, color) {
nWings = wings;
}

int main() {
FlyBug fb(,,);
fb.PrintBug();
fb.nType = ;
fb.nLegs = ; // error. nLegs is private
return ;
}

在创建派生类的对象时,需要调用基类的构造函数:初始化派生类对象中从基类继承的成员。

在执行一个派生类的构造函数之前,总是先执行基类的构造函数。

调用基类构造函数的两种方式:

  • 显式方式:用初始化列表。在派生类的构造函数中,为基类的构造函数提供参数     derived::derived(arg_derived-list) : base(arg_base-list)
  • 隐式方式:在派生类的构造函数中,省略基类构造函数时,派生类的构造函数则自动调用基类的默认构造函数(无参数构造函数).

派生类的析构函数被执行时,执行完派生类的析构函数后,自动调用基类的析构函数。

初始化/析构顺序:

在创建派生类的对象时:
1) 先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员;
2) 再执行成员对象类的构造函数,用以初始化派生类对象中成员对象。
3) 最后执行派生类自己的构造函数

在派生类对象消亡时:
1) 先执行派生类自己的析构函数
2) 再依次执行各成员对象类的析构函数
3) 最后执行基类的析构函数

析构函数的调用顺序与构造函数的调用顺序相反

class base { };
class derived : public base { };
base b;             //基类
derived d;         //派生类

派生类的对象可以赋值给基类对象                                                  b = d;             //默认将d的内容复制给b
派生类对象可以初始化基类引用                                                     base & br = d;    //br引用派生类中所包含的基类对象
派生类对象的地址可以赋值给基类指针                                           base * pb = & d;

public继承的赋值兼容规则:如果派生方式是 private或protected,则上述三条不可行


week6

虚函数

class base {
virtual int get() ;
};
int base::get(){
….
}
virtual 关键字只用在类定义里的函数声明中,写函数体时不用

多态:在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少

派生类的指针可以赋给基类指针。当通过基类指针调用基类和派生类中的同名虚函数时:
(1)若该指针指向一个基类的对象,那么被调用是基类的虚函数;
(2)若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。
这种机制就叫做“多态”。

class CBase {
public:
virtual void SomeVirtualFunction() { }
};
class CDerived:public CBase {
public :
virtual void SomeVirtualFunction() { }
};
int main() {
CDerived ODerived;
CBase * p = & ODerived;
p -> SomeVirtualFunction(); //调用哪个虚函数取决于p指向哪种类型的对象。这里是CDerived的SomeVirtualFunction
return ;
}

派生类的对象可以赋给基类的引用。通过基类引用调用基类和派生类中的同名虚函数时:
(1)若该引用引用的是一个基类的对象,那么被调用是基类的虚函数;
(2)若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。
这种机制也叫做“多态”。

class CBase {
public:
virtual void SomeVirtualFunction() { }
};
class CDerived:public CBase {
public :
virtual void SomeVirtualFunction() { }
};
int main() {
CDerived ODerived;
CBase & r = ODerived;
r.SomeVirtualFunction(); //调用哪个虚函数取决于r引用哪种类型的对象
return ;
}

多态” 的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定 ---- 这叫“动态联编” 。

多态的实现原理:虚函数表,用于存放该类的所有虚函数的地址,供运行时动态查询。

比如对于以下代码

class Base {
public:
int i;
virtual void Print() { cout << "Base:Print" ; }
};
class Derived : public Base{
public:
int n;
virtual void Print() { cout <<"Drived:Print" << endl; }
};

比如  pBase = pDerived;   pBase->Print();    ,因为pBase指向的是Derived类型的对象,所以去Derived的虚函数表中找Derived::Print();

虚析构函数

通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数。但是,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。
解决办法:把基类的析构函数声明为virtual
    派生类的析构函数可以virtual不进行声明
    通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数
一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义成虚函数。
注意: 不允许以虚函数作为构造函数

class son{
public:
virtual ~son() {cout<<"bye from son"<<endl;};
};
class grandson:public son{
public:
~grandson(){cout<<"bye from grandson"<<endl;};
};
int main() {
son *pson;
pson= new grandson();
delete pson;
return ;
}

输出:
  bye from grandson
  bye from son
执行grandson::~grandson(),引起执行son::~son()

包含纯虚函数的类叫抽象类
   抽象类只能作为基类来派生新类使用,不能创建抽象类的对象
   抽象类的指针和引用可以指向由抽象类派生出来的类的对象

纯虚函数: 没有函数体的虚函数
class A {
private: int a;
public:
virtual void Print() = ; //纯虚函数
void fun() { cout << "fun"; }
};

A a ; // 错, A 是抽象类,不能创建对象
A * pa ; // ok,可以定义抽象类的指针和引用
pa = new A ; // 错误, A 是抽象类,不能创建对象

在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部不能调用纯虚函数。

如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类。(可以类比java的抽象类:https://www.cnblogs.com/pdev/p/11188301.html

class A {
public:
virtual void f() = ; //纯虚函数
void g( ) { //抽象类中可以有普通成员函数和变量
this->f() ; //ok. f()是虚函数。因为抽象类A不能被实例化,所以这一步一定是通过派生类B走过来的,所以this指向的是B的对象
}
A( ){
f(); //会报错 构造函数/析构函数里调用的虚函数不是多态。所以此处调用的是A的f()
}
};

class B:public A{
public:
void f(){
cout<<"B:f()"<<endl;
}
};

int main(){
B b;
b.g();
return ;
}


week7

泛型程序设计:使用模板

template    类型参数表

template
void Swap(T & x,T & y){
T tmp = x;
x = y;
y = tmp;
}

template
T2 print(T1 arg1, T2 arg2)
{
cout<< arg1 << " "<< arg2<<endl;
return arg2;
}

template
T MaxElement(T a[], int size){ //size是数组元素个数
T tmpMax = a[];
for( int i = ;i < size;++i)
if( tmpMax < a[i] )
tmpMax = a[i];
return tmpMax;
}

template
T Inc(T n){
return + n;
}
int main(){
cout << Inc()/; //显式指定了将模板实例化成double类型。输出(4+1)/2=2.5
//如果没指定,因为4是int型,结果会是(4+1)/2=2
return ;
}

函数模板可以重载,只要它们的形参表或类型参数表不同即可。

template
void print(T1 arg1, T2 arg2) {
cout<< arg1 << " "<< arg2<
void print(T arg1, T arg2) {
cout<< arg1 << " "<< arg2<
void print(T arg1, T arg2) {
cout<< arg1 << " "<< arg2<<endl;
}

在有多个函数和函数模板名字相同的情况下, 编译器如下处理一条函数调用语句:
1) 先找参数类型完全匹配的普通函数 (非由模板实例化而得的函数)。
2) 再找参数类型完全匹配的模板函数。
3) 再找实参数经过自动类型转换后能够匹配的普通函数

template
T Max( T a, T b) {
cout << "TemplateMax" <
T Max( T a, T2 b) {
cout << "TemplateMax2" <<endl; return ;
}
double Max(double a, double b){
cout << "MyMax" << endl;
return ;
}
int main() {
int i=, j=;
Max( 1.2,3.4); // 输出MyMax
Max(i, j); //输出TemplateMax
Max( 1.2, ); //输出TemplateMax2
return ;
}

template
T myFunction( T arg1, T arg2){
cout<<arg1<<" "<<arg2<<"\n";
return arg1;
}
……
myFunction( , ); //ok: replace T with int
myFunction( 5.8, 8.4); //ok: replace T with double
myFunction( , 8.4); //error, no matching function for call to 'myFunction(int, double)' 匹配模板函数时,不进行类型自动转换

函数模板

类模板

111