C++程序设计概念

C++程序设计概念,第1张

本章分享《新标准C++程序设计》学习笔记,包括部分源码,北大郭炜编著,网上能搜到他们的系列公开课。

目录
  • 第一章,从C到C++
    • 引用
    • 内联函数
    • 指针和动态内存分配
    • delete运算符
    • string处理字符串
  • 第二章,类和对象
    • 类的定义
    • 访问对象的成员
    • 类成员的可访问范围
    • 构造函数 constructor
    • 默认构造函数 default constructor
    • 复制构造函数
    • 默认复制构造函数
    • 类型转换构造函数
    • 析构函数 destructor
    • 静态成员变量和静态成员函数
    • 常量对象和常量成员函数
    • 成员对象和封闭类
    • 封闭类的复制构造函数
    • 类的const成员和引用成员
    • 友元函数
    • 友元类
    • this指针
  • 第三章,运算符重载
    • +和-运算符重载
    • =运算符重载
    • 类型强制转换运算符重载
    • ++ 和 -- 运算符重载
  • 第四章,继承与派生
    • 继承与派生
    • protected访问范围说明符
    • 派生类的构造函数和析构函数
    • 共有派生的赋值兼容规则
  • 第五章,多态和虚函数
    • 多态
    • 通过基类指针实现多态
    • 通过基类引用实现多态
    • 多态的实现原理
    • 在成员函数中调用虚函数
    • 在构造函数和析构函数中调用虚函数
    • 区分多态和非多态情况
    • 虚析构函数
    • 纯虚函数和抽象类
  • 第六章,附录知识
    • 函数模板
    • 类模板
    • typename
    • C++类型转换
    • C++内存
    • C++智能指针
    • 指针函数
    • 函数指针
    • 函数对象(仿函数)
    • c++ 函数前面和后面 使用const 的作用

第一章,从C到C++ 引用

引用
定义方式:类型名 & 引用名=同类型的某变量名;例如,int n;int & r=n; r成为了n的引用,r的类型是int &。
定义引用时一定要初始化,引用只能引用变量,不能是常量或表达式;引用相当于该变量的一个别名
引用作为函数的返回值:函数的返回值可以是引用,例如,int & SetValue(){return n;},函数返回对n的引用
参数传引用:如果函数的形参是引用,那么参数的传递方式就是传引用的,形参是对应实参的引用,也就是说形参和实参是一回事,形参的改变会影响实参;例如,void Swap(int & a,int & b) {int tmp;tmp=a;a=b;b=tmp;},交换ab的值
常引用:定义引用时在前面加const关键字,该引用就成为常引用,例如,int n; const int & r=n; 定义了常引用r,其类型是const int &;不能通过常引用区修改其引用的内容
示例:参数传引用

#include
using namespace std;
void Swap(int & a, int & b)
{
	int temp;
	temp = a;
	a = b;
	b = temp;
}
int main()
{
	int n1 = 100, n2 = 50;
	Swap(n1, n2);//n1、n2的值被交换
	cout << n1 << " " << n2 << endl;//输出50 100
	return 0;
}
内联函数

在返回值类型前面加inline关键字,例如,inline int MAX(int a,int b) {if(a>b) return a; return b;}
执行时不会将该语句编译成函数调用的指令,而是直接将整个函数体代码插入调用语句处;可减少函数调用的开销,但会使最终可执行程序的体积增加;增加空间消耗来节省时间,适合很简单、执行很快的函数。

指针和动态内存分配

内存分配是在程序运行中进行的,不是在编译时进行的

P=new T; T是任意类型名,P是类型为T 的指针,语句动态分配一片大小为sizeof(T)字节的内存空间,并且将该内存空间的起始地址赋值给P
动态分配任意大小的数组,P=new T[N]; T是任意类型名,N代表元素个数,可以是任何值为正整数的表达式,这样的语句动态分配出N
sizeof(T)个字节的内存空间,这片空间的起始地址被赋值为P。

delete运算符

delete运算符,对动态分配所得的内存空间进行释放, 基本用法: delete 指针; 例如,int * p=new int;*p=5;delete p;
释放动态分配的数组,delete [ ]指针; 例如,int * p=new int[20];p[0]=0; delete [] p;
用new运算符动态分配的内存空间,一定要用delete运算符释放,否则会造成内存泄露,使系统变慢,但重启计算机可使这种情况消失。

string处理字符串

string 变量名; 可以在定义时初始化,例如,string str1;//定义了string对象str1。 string city=“Beijing”;//定义了string对象city,并对其初始化
还可以定义string对象数组,例如,string as[ ]={“Beijing”,“Shanghai”,“Chengdu”}; cout< string对象的输入输出,可以用cin、cout进行输入输出,例如,string s1, s2; cin>>s1>>s2; cout< string对象的赋值,string对象之间可以互相赋值,也可以用字符串常量和字符数组的名字对string对象赋值,例如,string s1, s2=“ok”; s1=“China”; s2=s1; char name[ ]=“Lady Xiong”; s1=name; //修改s1不会影响name
string对象的运算
string对象之间可以用“<“、“<=”、”==“、”>=“、“>”运算符进行比较,还可以用“+”将两个string对象相加、将一个字符串和string对象相加、将一个字符数组和string对象相加,相当于进行字符串连接。
“+=“运算符也适用于string对象,string对象还可以通过”[ ]“运算符和下标存取字符串中的某个字符 string对象是按词典序比较大小的,大小写相关,且大写字母的ASCII码小于小写字母的ASCII码
例如,string s1=“123” ,s2=“abc” ,s3; //s3是空串 s3=s1+s2; //s3变成"123abc” s3+=“de”; //s3变成"123abcde” bool b=s1 示例:

#include
#include
using namespace std;
int main()
{
	string s1 = "123", s2;//s2是空串
	s2 += s1;
	s1 = "abc";
	s1 += "def";
	cout << "1)" << s1 << endl;
	if (s2 < s1)
		cout << "2)s2 << endl;
	else
		cout << "2)s2>s1" << endl;
	s2[1] = 'A';
	s1 = "XYZ" + s2;
	string s3 = s1 + s2;
	cout << "3)" << s3 << endl;
	cout << "4)" << s3.size() << endl;
	string s4 = s3.substr(1, 3);//求s3从下标1开始,长度为3的子串
	cout << "5)" << s4 << endl;
	char str[20];
	strcpy(str, s4.c_str());//复制s4中的字符串到str
	cout << "6)" << str << endl;
	return 0;
}

打印输出:
1)abcdef
2)s2 3)XYZ1A31A3
4)9
5)YZ1
6)YZ1

第二章,类和对象

面向对象的程序设计有抽象、封装、继承、多态四个基本特点,多态是指不同种类的对象都具有名称相同的行为。

类的定义

类的定义, class 类名{ 访问范围说明符: 成员变量1 成员变量2 成员函数1 成员函数2 …}; 类的定义要以";"结束
一个类的成员函数之间可以相互调用;类的成员函数可以重载,也可以设定参数的默认值。以前所学的函数不是任何类的成员函数,可称为全局函数

成员函数的实现可以位于类的定义之外,格式:返回值类型 类名::函数名( ) { 语句组 } 定义对象的基本方法,类名 对象名; 对象名的命名规则和普通变量相同,对象也可以看作“类变量”。
在C++中,一个对象占用的内存空间的大小等于其 成员变量 所占用的内存空间的大小之和。成员函数并非每个对象各自存一份,成员函数和普通函数一样,在内存中只有一份,但它可以作用于不同的对象。

#include
using namespace std;
class CRectangle
{
public:
	int w, h;
	void init(int w_, int h_);
	int area();
	int perimeter();
};
void CRectangle::init(int w_, int h_)
{
	w = w_; h = h_;
}
int CRectangle::area()
{
	return w*h;
}
int CRectangle::perimeter()
{
	return 2 * (w + h);
}
int main()
{
	int w, h;
	CRectangle r;
	w = 10;
    h = 5;
	r.init(w, h);
	cout << "It's area is" << r.area() << endl;
	cout << "It's perimeter is" << r.perimeter() << endl;
	cout << sizeof(CRectangle) << endl;
	return 0;
}

打印:
It’s area is50
It’s perimeter is30
8

访问对象的成员

1、 对象名.成员名 例如,Crectangle r1 , r2 ; r1.w=5; r2.init(5,4);
2、 指针->成员名 例如,Crectangle r1 , r2 ; Crectangle * p1=&r1; Crectangle * p2=&r2; p1->w=5; p2->init(5,4);
3、 引用名.成员名 例如,Crectangle r2 ; Crectangle & rr=r2 ; rr.w = 5 ; rr.init(5,4) ; //rr的值改变,r2的值也改变

struct类, 将class关键字换成struct
没有成员函数的struct称作结构,结构变量不是对象,有成员函数的struct就是类。 对struct类来说,如果没有访问范围说明符,成员默认地被认为是共有成员。

类成员的可访问范围

private:用来指定私有成员。一个类的私有成员,不论是成员变量还是成员函数,都只能在该类的成员函数内部才能被访问。
public:用来指定共有成。一个类的共有成员在任何地方都可以被访问。
protected:用来指定保护成员。

构造函数 constructor

用于对对象进行自动初始化, 其名字和类的名字一样,不写返回值,可以重载,即一个类可以有多个构造函数。构造函数执行时,对象的内存空间已经分配好了,构造函数的作用是初始化这片空间。
示例,构造函数在数组中的使用:

#include
using namespace std;
class CSample
{
public:
	CSample()
	{
		cout << "Constructor 1 Called" << endl;
	}
	CSample(int n)
	{
		cout << "Constructor 2 Called" << endl;
	}
};
int main()
{
	CSample array1[2];
	cout << "step1" << endl;
	CSample array2[2] = { 4,5 };
	cout << "step2" << endl;
	CSample array3[2] = { 3 };
	cout << "step3" << endl;
	CSample* array4 = new CSample[2];
	delete[] array4;
	return 0;
}

打印:
Constructor 1 Called
Constructor 1 Called
step1
Constructor 2 Called
Constructor 2 Called
step2
Constructor 2 Called
Constructor 1 Called
step3
Constructor 1 Called
Constructor 1 Called

默认构造函数 default constructor

如果类的设计者没有写构造函数,编译器会自动生成一个没有参数的构造函数,该无参构造函数什么都不做。无参构造函数,不论是编译器自动生成的还是程序员写的,都成为默认构造函数。

复制构造函数

是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用。可以const引用,也可以是非const引用,一般使用前者。
定义,Complex( const Complex & c) { } Complex c2 (c1) ; //调用复制构造函数
示例:非默认复制构造函数

#include
using namespace std;
class Complex
{
public:
	double real, imag;
	Complex(double r, double i)
	{
		real = r; imag = i;
	}
	Complex(const Complex & c)//定义复制构造函数
	{
		real = c.real; imag = c.imag;
		cout << "Copy Constructor called" << endl;
	}
};
int main()
{
	Complex c1(1, 2);
	Complex c2(c1);//调用复制构造函数
	cout << c2.real << " " << c2.imag;
	return 0;
}

打印
Copy Constructor called
1 2

默认复制构造函数

如果类的设计者不写复制构造函数,编译器就会自动生成复制构造函数,称为默认复制构造函数。其作用是实现从源对象到目标对象逐个字节的复制,即使得目标对象的每个成员变量都变得和源对象相等

复制构造函数被调用的三种情况
1、当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用
例如,Complex c2 (c1) ; Complex c2=c1 ; //第二条语句是初始化语句,不是赋值语句。赋值语句的等号左边是一个已定义的变量,赋值语句不会引发复制构造函数调用,例如 Complex c1,c2 ; c1=c2; c1早已生成,已经初始化了,不调用复制构造函数
2、作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。例如,void Func (A a) { }
如果函数F的参数是类A的对象,那么当F被调用时,类A的复制构造函数将被调用。对象作为函数的形参,要用复制构造函数初始化,这会带来时间上的开销,如果用对象的引用而不是对象作为形参,就没有这个问题了,可用const引用。
3、作为函数返回值的对象是用复制构造函数初始化的,而调用复制构造函数时的实参,就是return语句所返回的对象。如果函数的返回值是类A的对象,则函数返回时,类A的复制构造函数被调用
例如,A Func( ) { A a(4) ; return a; } //调用Func函数时,其返回值是一个对象,该对象就是用复制构造函数初始化的,而且调用复制构造函数时,实参就是return语句所返回的a。
示例:复制构造函数用于函数形参

#include
using namespace std;
class A
{
public:
	A() {};
	A(A &a)
	{
		cout << "Copy constructor called" << endl;
	}
};
void Func(A a)
{

}
int main()
{
	A a;
	Func(a);
	return 0;
}

打印
Copy constructor called

示例:复制构造函数用于函数返回值

#include
using namespace std;
class A
{
public:
	int v;
	A(int n)
	{
		v = n;
	}
	A(const A &a)
	{
		v = a.v;
		cout << "Copy constructor called" << endl;
	}
};
A a(4);//不同的编译器会有差异,这里使用mgw640编译器,只有把a定义为全局变量才会调用复制构造函数
A Func()
{
	//A a(4);//vs编译器时,定义为局部变量也会调用复制构造函数
	return a;
}
int main()
{
	cout << Func().v << endl;
	return 0;
}

打印
Copy constructor called
4

类型转换构造函数

除复制构造函数外,只有一个参数的构造函数一般都可以称作类型转换构造函数,因为这样的构造函数能起到类型自动转换的作用。
例如,定义,Complex ( int i) { } 调用,Complex c1(7,8) ; Complex c2=12 ; c1=9; 后面两条语句均调用类型转换构造函数,最后一条赋值语句等号两边的类型不匹配,因为Complex(int)这个类型转换构造函数能接受一个整型参数
编译器在处理c1=9; 这条赋值语句时,会在等号右边自动生成一个临时的Complex对象,该临时对象以9为实参,用Complex(int)构造函数初始化,然后再将这个临时对象赋值给c1,也就是说9被自动转换成一个Complex对象再赋值给c1.
示例:类型转换构造函数

#include
using namespace std;
class Complex {
public:
	double real, imag;
	Complex(int i)//类型转换构造函数
	{
		cout << "IntConstructor called" << endl;
		real = i; imag = 0;
	}
	Complex(double r, double i)
	{
		real = r; imag = i;
	}
};
int main()
{
	Complex c1(7, 8);
	Complex c2 = 12;
	c1 = 9;//9被自动转换成一个临时Complex 对象
	cout << c1.real << "," << c1.imag << endl;
	return 0;
}

打印
IntConstructor called
IntConstructor called
9,0

析构函数 destructor

析构函数是成员函数的一种,它的名字与类名相同,但前面要加“~”,没有参数和返回值。 一个类有且仅有一个析构函数,如果定义类时没写析构函数,则编译器生成默认析构函数。
析构函数在对象消亡时自动被调用,可以定义析构函数在对象消亡前做善后工作。函数的参数对象以及作为函数返回值的对象,在消亡时也会引发析构函数的调用。例如,class String{public: String (int n) ; ~String(); }

#include
using namespace std;
class CDemo {
public:
	~CDemo()
	{
		cout << "Destructor called" << endl;
	}
};
int main()
{
	CDemo array[2];//构造函数调用2次
	CDemo * pTest = new CDemo;//构造函数调用1次
	delete pTest;//析构函数调用
	cout << "**********" << endl;
	pTest = new CDemo[2];//构造函数调用2次
	delete[] pTest;//析构函数调用2次
	cout << "Main ends" << endl;
	return 0;
}

打印
Destructor called


Destructor called
Destructor called
Main ends
Destructor called
Destructor called

静态成员变量和静态成员函数

在成员变量定义和成员函数声明前面加了 static关键字。例如,class Crectangle { private: int w,h; static int nTotalArea; //静态成员变量 public: static void PrintTotal ( ) ; //定义成员函数 }
普通成员变量每个对象有各自的一份,而静态成员变量只有一份,被所有同类对象共享。普通成员函数一定是作用在某个对象上,而静态成员函数并不具体作用在某个对象上。访问静态成员,“类名::成员名”。
静态成员变量本质上是全局变量,哪怕一个对象都不存在,其静态成员变量也存在。使用sizeof运算符计算对象所占用的存储空间时,不会将静态成员变量计算在内。
注意,静态成员变量必须在类定义的外面专门声明,声明时变量名前面加“类名::”。因为静态成员函数不具体作用于某个对象,静态成员函数内部不能访问非静态成员变量,也不能调用非静态成员函数。

#include
using namespace std;
class CRectangle {
private:
	int w, h;
	static int totalArea;//总面积
	static int totalNumber;//总数量
public:
	CRectangle(int w_, int h_);
	~CRectangle();
	static void PrintTotal();
};
CRectangle::CRectangle(int w_, int h_)
{
	w = w_; h = h_;
	totalNumber++;
	totalArea += w*h;
}
CRectangle::~CRectangle()
{
	totalNumber--;
	totalArea -= w*h;
}
void CRectangle::PrintTotal()
{
	cout << totalNumber << "," << totalArea << endl;
}
//必须在定义类的文件中对静态成员变量进行一次声明
//或初始化,否则编译能通过,链接不能通过
int CRectangle::totalNumber = 0;
int CRectangle::totalArea = 0;
int main()
{
	CRectangle r1(3, 3), r2(2, 2);
	CRectangle::PrintTotal();
	r1.PrintTotal();
	return 0;
}

打印
2,13
2,13

常量对象和常量成员函数

定义对象时在前面加const关键字,使之成为常量对象。不能通过常量对象调用普通成员函数,但可以通过常量对象调用常量成员函数。所谓常量成员函数,就是在定义和声明时加了const关键字的成员函数。
例如,class Sample { public:void GetValue( ) const ; }; void Sample :: GetValue( ) const { } int main( ) { const Sample o ; o.GetValue( ) ; return 0; } //常量对象上可执行常量成员函数

#include
using namespace std;
class Sample {
public:
	void GetValue() const;
};
void Sample::GetValue()const
{
}
int main()
{
	const Sample o;
	o.GetValue();//常量对象上可以执行常量成员函数
	return 0;
}
成员对象和封闭类

一个类的成员变量如果是另一个类的对象,就称之为成员对象。包含成员对象的类叫封闭类(enclosed class)。

封闭类构造函数的初始化列表
当封闭类的对象生成并初始化时,它包含的成员对象也需要被初始化,这就会引发成员对象构造函数的调用。
类名::构造函数名(参数表):成员变量1(参数表),成员变量2(参数表), { } 初始化列表中的成员变量既可以是成员对象,也可以是基本类型的成员变量。对于成员变量,参数表中存放的是构造函数的参数。
对于基本类型成员变量,参数表中就是一个初始值。例如,class Ctyre { int radius,width; public: Ctyre( int r, int w) : radius( r ),width ( w ){ } //成员变量初始化 };
class Ccar { int price; Ctyre tyre ; public: Ccar (int p,int tr , int tw) } ; Ccar :: Ccar (int p ,int tr , int tw ) :price ( p ) , tyre ( tr,tw) {};
示例

#include
using namespace std;
class CTyre
{
private:
	int radius;
	int width;
public:
	CTyre (int r,int w):radius(r),width(w){}//成员变量初始化
};
class CEngine
{};
class CCar {
private:
	int price;
	CTyre tyre;
	CEngine engine;
public:
	CCar(int p, int tr, int tw);
};
CCar::CCar(int p, int tr, int tw) :price(p), tyre(tr, tw)
{};
int main()
{
	CCar car(20000, 17, 225);
	return 0;
}

封闭类对象生成时,先执行所有成员对象的构造函数,然后才执行封闭类自己的构造函数;成员对象构造函数的执行次序和成员对象在类定义中的次序一致。
当封闭类对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数,成员对象析构函数的执行次序和构造函数的执行次序相反,即先构造的后析构,这也是C++处理此类次序问题的一般规律。

封闭类的复制构造函数

封闭类的对象,如果是用默认复制构造函数初始化的,那么它包含的成员对象也会用复制构造函数初始化。

#include
using namespace std;
class A
{
public:
	A() { cout << "default" << endl; }
	A(A & a) { cout << "copy" << endl; }
};
class B
{
	A a;
};
int main()
{
	B b1, b2(b1);
	return 0;
}

打印
default
copy

类的const成员和引用成员

类还可以有常量成员变量和引用型成员变量。这两种类型的成员变量必须在构造函数的初始化列表中进行初始化。常量型成员变量的值一旦初始化就不能再改变。

#include
using namespace std;
int f;
class CDemo {
private:
	const int num;
	int & ref;
	int value;
public:
	CDemo(int n):num(n),ref(f),value(4){}
};
int main()
{
	cout << sizeof(CDemo) << endl;
	return 0;
}

打印
12

友元函数

在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为友元,这些函数就成为该类的友元函数,在友元函数内部就可以访问该类对象的私有成员了。
将全局函数声明为友元: friend 返回值类型 函数名(参数表);
将其他类的成员函数声明为友元: friend 返回值类型 其他类的类名::成员函数名(参数表);

友元类

一个类A可以将另一个类B声明为自己的友元,类B的所有成员函数就都可以访问类A对象的私有成员。 写法,friend class 类名;

this指针

非静态成员函数实际上的形参个数比程序员写的多一个“this指针”,这个this指针指向了成员函数作用的对象。例如,this指针的类型是Complex * ; *this 代表函数所作用的对象。

第三章,运算符重载

运算符重载的实质是编写以运算符作为名称的函数。目的是使得C++中的运算符也能用来 *** 作对象。 返回值类型 operator 运算符(形参表) { }
包含被重载的运算的表达式会被编译成对运算符函数的调用,运算符的 *** 作数成为函数调用时的实参,运算的结果就是函数的返回值。
例如,Complex operator + ( const Complex &a , const Complex &b ) { return Complex (a.real+b.real , a.imag+b.imag) ; //返回一个临时对象 } c=a+b;就等价于c=operator+(a,b)

+和-运算符重载
#include
using namespace std;
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(4, 4), b(1, 1), 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 0;
}
=运算符重载
#include
#include
using namespace std;
class String
{
private:
	char *str;
public:
	String():str(NULL){}
	const char *c_str()const { return str; };
	String & operator=(const char *s);
	~String();
};
String & String::operator=(const char *s)
{
	if (str)
		delete[]str;
	if (s)
	{
		str = new char[strlen(s) + 1];
		strcpy(str, s);
	}
	else
		str = NULL;
	return *this;
}
String ::~String()
{
	if (str)
		delete[]str;
}
int main()
{
	String s;
	s = "Good luck!";//等价于s.operator=("Good Luck,");
	cout << s.c_str() << endl;
	s = "Shenzhou!";
	cout << s.c_str() << endl;
	return 0;
}
类型强制转换运算符重载
#include
using namespace std;
class Complex {
	double real, imag;
public:
	Complex(double r = 0, double i = 0) :real(r), imag(i) {};
	operator double() { return real; }//重载类型强制转换运算符double
};
int main()
{
	Complex c(1.2, 3.4);
	cout << (double)c << endl;
	double n = 2 + c;//等价于double n=2+c.operator double()
	cout << n;
	return 0;
}

打印
1.2
3.2

++ 和 – 运算符重载
#include
using namespace std;
class CDemo {
private:
	int n;
public:
	CDemo(int i=0):n(i){}
	CDemo & operator++();
	CDemo operator++(int);
	operator int() { return n; }
	friend CDemo & operator--(CDemo &);
	friend CDemo operator--(CDemo &, int);
};
CDemo & CDemo::operator++()
{
	n++;
	return *this;
}
CDemo CDemo::operator++(int k)
{
	CDemo tmp(*this);
	n++;
	return tmp;
}
CDemo & operator--(CDemo &d)
{
	d.n--;
	return d;
}
CDemo operator--(CDemo & d, int)
{
	CDemo tmp(d);
	d.n--;
	return tmp;
}
int main()
{
	CDemo d(5);
	cout << (d++) << ",";//等价于d.operator++(0)
	cout << d << ",";
	cout << (++d) << ",";//等价于d.operator++()
	cout << d << ",";
	return 0;
}
第四章,继承与派生 继承与派生

派生类时提供对基类进行扩充和修改得到的。 基类的所有成员自动成为派生类的成员。 所谓扩充,指的是派生类中可以添加新的成员变量和成员函数。所谓修改,指的是派生类中可以重写从基类继承得到的成员。
在C++中,派生的写法, class 派生类名:继承方式说明符 基类名 { }
继承方式说明符可以是public、private、protected,一般都使用public。派生类的成员函数不能访问基类的私有成员。
在基类和派生类有同名成员的情况下,在派生类的成员函数中访问同名成员,访问的就是派生类的成员,这种情况叫“覆盖”,即派生类的成员覆盖基类的同名成员。如要访问基类的同名成员,需要在成员名前加“基类名::”
在派生类的同名成员函数中,先调用基类的同名成员函数完成基类部分的功能,然后再执行自己的代码完成派生类的功能。
示例:

#include
#include
using namespace std;
class CStudent {
private:
	string name;
	string id;
	char gender;
	int age;
public:
	void PrintInfo();
	void SetInfo(const string & name_, const string id_, int age_, char gender_);
	string GetName() { return name; }
};
class CUndergraduateStudent :public CStudent {
private:
	string department;
public:
	void QulifiedForBaoyan() {//保研资格
		cout << "qulified for baoyan" << endl;
	}
	void PrintInfo() {
		CStudent::PrintInfo();//调用基类的PrintInfo
		cout << "Department:" << department << endl;
	}
	void SetInfo(const string & name_, const string id_, int age_, char gender_, const string & department_) {
		CStudent::SetInfo(name_, id_, age_, gender_);//调用基类的SetInfo
		department = department_;
	}
};
void CStudent::PrintInfo() {
	cout << "Name:" << name << endl;
	cout << "Id:" << id << endl;
	cout << "Age:" << age << endl;
	cout << "Gender:" << gender << endl;
}
void CStudent::SetInfo(const string & name_, const string id_, int age_, char gender_) {
	name = name_;
	id = id_;
	age = age_;
	gender = gender_;
}
int main()
{
	CStudent s1;
	CUndergraduateStudent s2;
	s2.SetInfo("Harry Potter", "118829212", 19, 'M', "Computer Science");
	cout << s2.GetName() << " ";
	s2.QulifiedForBaoyan();
	s2.PrintInfo();
	cout << "size of string=" << sizeof(string) << endl;
	cout << "size of CStudent=" << sizeof(CStudent) << endl;
	cout << "size of =CUndergraduateStudent" << sizeof(CUndergraduateStudent) << endl;
	return 0;
}

打印:
Harry Potter qulified for baoyan
Name:Harry Potter
Id:118829212
Age:19
Gender:M
Department:Computer Science
size of string=24
size of CStudent=56
size of =CUndergraduateStudent80

protected访问范围说明符

保护成员的可访问范围比私有成员大,比共有成员小。能访问私有成员的地方都能访问保护成员。保护成员扩大的访问范围:基类的保护成员可以在派生类的成员函数中被访问。

派生类的构造函数和析构函数

派生类对象在创建时,除了要调用自身的构造函数进行初始化外,还要调用基类的构造函数初始化其包含的基类对象。
和封闭类初始化类似,在构造函数后面添加初始化列表。 构造函数名(形参表):基类名(基类构造函数实参表)
在执行派生类的构造函数之前,总是先执行基类的构造函数。 派生类对象消亡时,先执行派生类的析构函数,再执行基类的析构函数。
示例,派生类的构造函数和析构函数调用顺序:

#include
#include
using namespace std;
class CBug
{
	int legNum, color;
public:
	CBug(int ln,int cl):legNum(ln),color(cl)
	{
		cout << "CBug Constructor" << endl;
	}
	~CBug()
	{
		cout << "CBug Destructor" << endl;
	}
	void PrintInfo()
	{
		cout << legNum << "," << color << endl;
	}
};
class CFlyingBug :public CBug
{
	int wingNum;
public:
	//CFlyingBug(){}//报错
	/*CFlyingBug::*/CFlyingBug(int ln,int cl,int wn):CBug(ln,cl),wingNum(wn)
	{
		cout << "CFlyingBug Constructor" << endl;
	}
		~CFlyingBug()
	{
		cout << "CFlyingBug Destructor" << endl;
	}
};
int main()
{
	CFlyingBug fb(2, 3, 4);
	fb.PrintInfo();
	return 0;
}

打印:
CBug Constructor
CFlyingBug Constructor
2,3
CFlyingBug Destructor
CBug Destructor

共有派生的赋值兼容规则

1、派生类的对象可以赋值给基类对象
2、派生类对象可以用来初始化基类引用
3、派生类对象的地址可以赋值给基类指针,亦即派生类的指针可以赋值给基类的指针
例如,两哥类的对象a=b; 在赋值号“=” 没有被重载的情况下,所做的 *** 作就是将派生类对象中的基类对象逐个字节地复制到“=”左边的基类对象中。

第五章,多态和虚函数 多态

多态(polymorphism)指的是同一名字的事物可以完成不同的功能。多态可以分为编译时的多态和运行时的多态。
编译时的多态:指函数的重载、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数。运行时的多态,和继承、虚函数等相关。

通过基类指针实现多态

派生类对象的地址可以赋值给基类指针。通过基类指针调用基类和派生类中都有的同名、同参数表的虚函数的语句,编译时并不确定要执行的是基类还是派生类的虚函数。在程序运行时,看基类指针指向的是基类对象还是派生类对象。
所谓虚函数,就是在声明时前面加了virtual关键字的成员函数
virtual关键字只在类定义中的成员函数声明处使用。静态成员函数不能是虚函数。 包含虚函数的类成为“多态类”。

通过基类引用实现多态

通过基类的引用调用虚函数的语句也是多态的。例如,class A{virtual void Print(){} };class B:public A {virtual void Print(){} }; void PrintInfo(A & r) { r.Print(); } //多态,调用哪个Print,取决于r引用了哪个类的对象 PrintIInfo(a);

#include
using namespace std;
class A
{
public:
	virtual void Print() { cout << "A::Print" << endl; }
};
class B :public A
{
public:
	virtual void Print() { cout << "B::print" << endl; }
};
void PrintInfo(A &r)
{
	r.Print();//多态,调用哪个Print取决于r引用了哪个类的对象
}
int main()
{
	A a; B b;
	PrintInfo(a);//输出A::Print
	PrintInfo(b);//输出B::Print
	return 0;
}
多态的实现原理

有了虚函数,对象所占用的存储空间比没有虚函数时多了4个字节,这4个字节位于对象存储空间的最前端,其中存放的是虚函数表的地址。
每一个有虚函数的类都有一个虚函数表,该类的任何对象中都放着该虚函数表的指针。虚函数表中列出了该类的全部虚函数地址。多态的函数调用语句被编译成根据基类指针所指向的对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用。

在成员函数中调用虚函数

类的成员函数之间可以相互调用。在成员函数中调用其他虚成员函数的语句是多态的(静态成员函数、构造函数、析构函数除外)

#include
using namespace std;
class CBase
{
public:
	void func1() { func2(); }
	virtual void func2() { cout << "CBase::func2()" << endl; }
};
class CDerived :public CBase
{
public:
	virtual void func2() { cout << "CDerived:func2()" << endl; }
};
int main()
{
	CDerived d;
	d.func1();
	return 0;
}

打印
CDerived:func2()

在构造函数和析构函数中调用虚函数

在构造函数和析构函数中调用虚函数不是多态的。如果本类有该函数,调用的就是本类的函数;如果本类没有,调用的就是直接基类的函数;如果直接基类没有,调用的就是间接基类的函数。

#include
using namespace std;
class A
{
public:
	virtual void hello() { cout << "A::hello" << endl; }
	virtual void bye() { cout << "A::bey" << endl; }
};
class B :public A
{
public:
	virtual void hello() { cout << "B::hello" << endl; }
	B() { hello(); }
	~B() { bye(); }
};
class C :public B
{
public:
	virtual void hello() { cout << "C::hello" << endl; }
};
int main()
{
	C obj;
	return 0;
}

打印
B::hello
A::bye

区分多态和非多态情况

通过基类指针或引用调用成员函数的语句,只有当该成员函数时虚函数时才会是多态。 另C++规定,只要基类中的某个函数被声明为虚函数,则派生类中的同名、同参数表的成员函数即使前面不写virtual关键字,也自动成为虚函数。

#include
using namespace std;
class A
{
public:
	void func1() { cout << "A::func1" << endl; }
	virtual void func2() { cout << "A::func2" << endl; }
};
class B :public A
{
public:
	virtual void func1() { cout << "B::func1" << endl; }
	void func2() { cout << "B::func2" << endl; }//虚函数
};
class C :public B
{
public:
	void func1() { cout << "C::func1" << endl; }//虚函数
	void func2() { cout << "C::func2" << endl; }//虚函数
};
int main()
{
	C obj;
	A * pa = &obj;
	B * pb = &obj;
	pa->func2();//多态
	pa->func1();//不是多态
	pb->func1();//多态
	return 0;
}

打印
C::func2
A::func1
C::func1

虚析构函数

只要基类的析构函数是虚函数,那么派生类的析构函数不论是否用virtual关键字,都自动成为虚析构函数。一个类如果定义了虚函数,则最好将析构函数也定义成虚函数。
派生类的析构函数会自动调用基类的析构函数。析构函数可以是虚函数,但是构造函数不能是虚函数。

#include
using namespace std;
class CShape
{
public:
	virtual ~CShape() { cout << "CShape::destructor" << endl; }
};
class CRectangle :public CShape
{
public:
	int w, h;
	~CRectangle() { cout << "CRectangle::destructor" << endl; }
};
int main()
{
	CShape* p = new CRectangle;
	delete p;
	return 0;
}

打印
CRectangle::destructor
CShape::destructor

纯虚函数和抽象类

纯虚函数就是没有函数体的虚函数。包含纯虚函数的类就叫抽象类。
纯虚函数的写法就是在函数声明后面加“=0”,不写函数体。例如,virtual void Print()=0; 抽象类不能生成独立的对象。
可以定义抽象类的指针或引用,并让它们指向或引用抽象类的派生类的对象,以实现多态。 如果一个类从抽象类派生而来,那么当且仅当它对基类中的所有纯虚函数都进行覆盖并都写出函数体,它才能成为非抽象类。

第六章,附录知识 函数模板

函数模板是用于生成函数的,写法, template 返回值类型 模板名 (形参表) {函数体}
例如,template void Swap(T & x , T & y ) {T tmp=x; x=y; y=tmp; } //调用 Swap(n,m); 调用时会用具体的类型名对模板中所有的类型参数进行替换,其他部分原封不动的保留。
编译器由模板自动生成函数的过程叫模板的实例化。由模板实例化而得到的函数成为模板函数。

类模板

人们需要编写多个形式和功能都相似的类,于是C++引入类模板的概念,编译器可以从类模板自动生成多个类。
C++中类模板的写法, template class 类模板名{ 成员函数和成员变量 }
模板中的成员函数放到类模板定义外面时的写法,template<类型参数表> 返回值类型 类模板名<类型参数名列表>::成员函数名(参数表) { } 例如,template bool Pair::operator<(const Pair & P)const {return key 用类模板定义对象的写法, 类模板名<真实类型参数表> 对象名 (构造函数实际参数表);例如,Pairstudent (“Tom” , 19);
如果类模板有无参构造函数,也可以使用如下写法, 类模板名<真实类型参数表> 对象名;

编译器由类模板生成类的过程叫类模板的实例化。由类模板实例化得到的类叫模板类。
函数模板和类模板介绍

typename

typename:是一个C++程序设计语言中的关键字,相当用于泛型编程时是另一术语"class"的同义词。这个关键字用于指出模板声明(或定义)中的依赖的名称(dependent names)是类型名,而非变量名
typedef用法,为现有类型创建一个新的名字
1、定义一种类型的别名,而不只是简单的宏替换
char* pa, pb; // 这多数不符合我们的意图,它只声明了一个指向字符变量的指针, 和一个字符变量
typedef char* PCHAR; // 一般用大写
PCHAR pa, pb; // 可行,同时声明了两个指向字符变量的指针
2、旧的C代码声明struct新对象时: struct 结构名 对象名,而C++中可以直接写:结构名 对象名
利用typedef的简化写法:typedef struct tagPOINT {…}POINT; 这样定义结构体对象时就可以写成:POINT p1;
3、用typedef来定义与平台无关的类型。
比如定义一个叫 REAL 的浮点类型,在目标平台一上,让它表示最高精度的类型为:
typedef long double REAL;
在不支持 long double 的平台二上,改为:
typedef double REAL;
在连 double 都不支持的平台三上,改为:
typedef float REAL;
也就是说,当跨平台时,只要改下 typedef 本身就行,不用对其他源码做任何修改。
标准库就广泛使用了这个技巧,比如size_t。
另外,因为typedef是定义了一种类型的新别名,不是简单的字符串替换,所以它比宏来得稳健

C++类型转换

const_cast:去掉类型的const或volatile属性,去const属性
static_cast:无条件转换,静态类型转换,基本类型转换,类似C的强转Type b = (Type)a
dynamic_cast:有条件转换,动态类型转换,运行时检查类型安全(转换失败返回NULL),多态类之间的类型转换
reinterpret_cast:仅重新解释类型,但没有进行二进制的转换,不同类型的指针类型转换

C++内存

1.栈,那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等
2.堆,那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后, *** 作系统会自动回收
3.自由存储区,那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
4.全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
5.常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量

C++智能指针

unique_ptr,由 C++11 引入,旨在替代不安全的 auto_ptr,它持有对对象的独有权——两个unique_ptr 不能指向一个对象,即 unique_ptr 不共享它所管理的对象,只能移动 unique_ptr,即对资源管理权限可以实现转移
auto_ptr,由 C++98 引入,auto_ptr有赋值语义,赋值后则两个指针将指向同一个 对象。这是不能接受的,因为程序将试图删除同一个对象两次,因此auto_ptr是不安全的
shared_ptr,是一个标准的共享所有权的智能指针,允许多个指针指向同一个对象,shared_ptr 利用引用计数的方式实现了对所管理的对象的所有权的分享,即允许多个 shared_ptr 共同管理同一个对象
weak_ptr,被设计为与 shared_ptr 共同工作,可以从一个 shared_ptr 或者另一个 weak_ptr 对象构造而来,它的最大作用在于协助 shared_ptr 工作,可获得资源的观测权, weak_ptr 可用于打破循环引用

指针函数

顾名思义,它的本质是一个函数,不过它的返回值是一个指针
ret *func(args, …);
其中,func是一个函数,args是形参列表,ret *作为一个整体,是 func函数的返回值,是一个指针的形式。

函数指针

函数指针 的本质是一个指针,该指针的地址指向了一个函数,所以它是指向函数的指针
我们知道,函数的定义是存在于代码段,因此,每个函数在代码段中,也有着自己的入口地址,函数指针就是指向代码段中函数入口地址的指针
ret (*p)(args, …);
其中,ret为返回值,*p作为一个整体,代表的是指向该函数的指针,args为形参列表。其中p被称为函数指针变量 。
举例:
double cal(int); // prototype
double (*pf)(int); // 指针pf指向的函数, 输入参数为int,返回值为double
pf = cal; // 指针赋值
void estimate(int lines, double (*pf)(int)); // 函数指针作为参数传递,函数名就是指针,调用时直接传入函数名:estimate(line_num, cal_m1);
double y = cal(5); // 通过函数调用
double y = (*pf)(5); // 通过指针调用 推荐的写法

函数对象(仿函数)

函数对象:定义了调用 *** 作符()的类对象。当用该对象调用此 *** 作符时,其表现形式如同普通函数调用一般,因此取名叫函数对象。

    class A 
    {
    public:
        int operator() ( int val ) 
        {
            return val > 0 ? val : -val;
        }
    };

调用:
int i = -1;
A func;
cout << func(i);

c++ 函数前面和后面 使用const 的作用

前面使用const 表示返回值为const
后面加 const表示函数不可以修改class的成员

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/793849.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-06
下一篇 2022-05-06

发表评论

登录后才能评论

评论列表(0条)

保存