零散的C++笔记

看前必读

    这是我个人整理的一些C++笔记,仅记录一些个人认为易忘的知识点,因此并不完整。其中可能出现一些错误,我发现会尽快纠正。其中包含的大部分代码均为演示使用,代码仅摘取关键部分。

1 引用

    引用是指向某一数据块的特殊变量,它类似指针但区别于指针。引用不占据内存空间,它必须与某一变量关联,指向某变量的数据块。引用一旦初始化(也必须初始化与某一变量指向同一数据块)则不可再次修改指向,声明格式如下:

int a;
int& b = a;	//此时 b 是 a 的一个引用

    引用作为函数参数传递时,函数内部对其的修改也会直接修改 a 的数据块(因为它们本来就是指向的同一个数据块),局部变量的引用不能作为返回值,因为函数内部的变量为局部变量,函数返回时局部变量失效,此时内部的引用则是指向了一块非法的地址。

2 函数

2.1 函数重载

    函数名相同,参数列表不相同。可以是参数类型,参数数量、参数类型数量相同但顺序不同。这样的函数称为互为重载函数,根据用户调用传递的参数匹配符合的函数。
注:返回值类型不作为重载函数的条件

void fun(int a, float b)
{
	...
}

int fun(int a)
{
	...
} 

2.2 函数默认参数值

    在声明函数时可以指定参数默认值,需遵循从右至左、连续指定原则,在调用函数时可省去对应位的参数传递。
注:对指定默认值的函数调用时注意重载函数

void fun(int a, int b = 1; float f = 1.23f)	//标准函数参数默认值指定格式
{
	...
}

如有另一函数与上函数互为重载函数:
void fun(int a, int b)
{
	...
}
当调用格式如 fun(10,20) 时,就会出现歧义


3 类

    类是C++ (相较于C) 特有的性质,类内部可能包含多种数据成员,类似于结构体,但又不同于结构体,结构体仍可以作为类的一个成员。

3.1 外部函数成员

    类内的函数成员可以在类外定义,但在类内必须写对应的函数声明。

// 类内声明
返回值类型 函数名(参数列表);

// 外部定义格式
返回值类型 类名::函数名(参数列表)
{
	...
}

3.2 访问修饰符

public:类外可见,结构体内成员默认为public
protected:类内可见,友元及子类可见
private:类内可见,友元可见,类内成员默认为private

3.3 继承

    某些情况下,如有许多个类,但是这些类都有一些共同的属性,此时在每个类中单独实现这些功能显得比较冗杂。因此,常常把相同的 属性/功能 单独在一个类中实现,其他所需要这个 属性/功能 的类从这个类继承。被继承的类称之为基类或者父类,继承别人的类称之为派生类或者子类。
    一个子类可以继承多个父类,多个父类用逗号隔开,一个父类可以有多个子类。同样子类也可以被继承,即允许子类还有子类。注意:使用子类创建对象时构造函数调用顺序从父类到子类,析构函数从子类到父类。父类构造函数如果有参数,在子类构造函数后(初始化列表)进行传递,格式如下:

class CSon : public CFather
{
	...
	CSon() : CFather(参数列表)	//参数列表:传入父类构造函数的参数列表
	{
	}
};

继承限定词

public: 继承父类中的 public 修饰的成员,在子类中仍为 public 修饰;
protected: 继承 protected 以及 public 修饰符修饰的成员,在子类均为 protected 修饰的成员;
private: 继承父类 public 和 protected 修饰的成员,在子类中均为 private 修饰的成员,而父类的 private 成员对子类不可见。

class CFather	//父类
{
public:
	int n1;
protected:
	int n2;
private:
	int n3;
};

// 子类1,pyblic继承限定词
class CSon1 : public CFather
{
	...
	//仅继承父类的 n1 成员,在这里为 public 修饰成员
};

//子类2,protected继承限定词
class CSon2 : protected CFather
{
	...
	//父类的 n1 和 n2 在这里等同于被 protected 修饰的成员
};

//子类3,private继承限定词
class CSon3 : private CFather	
{
	...
	//父类的 n1 和 n2 在这里等同于被 private 修饰的成员
};
3.3.1 虚继承

    在关系较为复杂的继承关系中,普通继承可能会出现一些问题。如下继承关系:A为基类,B、C 继承 A,D又继承 B 和 C。当使用 D 实例化对象后,使用该对象调用属于 A 的成员(假如为 fun())时,则会出现歧义,因为 B 和 C 中均有一份 fun()。因此 B 和 C 使用虚继承,使得 B 和 C 并非真正的从 A 获得成员,而是类似于授权 A 类成员使用权使用权的方式访问成员,于是 D 在继承 B、C后使用该成员不会出现歧义。虚继承格式如下:

class B : virtual public A
{
	...
};

class C : virtual public A
{
	...
};

3.4 友元

    要想使用类中 protected 或 private 修饰的成员,可以使用声明友元的方式,友元声明在被需要访问成员的类中,友元可以是函数,也可以是类。友元一定程度上破坏了访问修饰符的限制作用,不建议使用。

class CStu1
{
	...
	friend void fun1();	//友元函数声明格式,friend 后跟函数原型
	friend class CStu2;	//友元类声明格式,friend class 后跟类名
};

void fun1()
{
	...
}

class Stu2
{
	...
};

3.5 接口函数

    除了以上方式访问类中成员,其实可以使用接口函数的形式访问类内部 protected 或 private 修饰的成员。其本质是在定义类原本所需的成员后,追加public(根据需要而定,也可以是 protected,如果是 private,则接口函数无意义)修饰的“接口函数”,此函数有权限访问类内成员,外部仅需通过此函数间接访问类内成员。例如:

class CID
{
private:
	int id;
public:
	int fun(int value)
	{
		id = value;
	}
};
注:当外部声明一个对象,然后调用 fun 函数时即可获得该对象“私有”的 id 成员进行操作

3.6 类的函数成员
3.6.1 构造函数

    在某些场景,需要对类内成员进行初始化,可以在类内声明一个函数,在函数内部对类内成员赋值(不可在类内声明成员时对成员进行初始化),但是其缺陷是每次声明一个对象,需要手动调用一次该函数以完成数据初始化。
   构造函数可以解决这个问题,在声明对象过程中,程序会自动调用构造函数对数据成员进行赋值。对于指针对象,仅有遇到 new 语句才会触发构造函数 (malloc不会触发构造函数) 。

class CStu
{
public:
	int age;
	char gender;
	CStu()	//构造函数,类名作为函数名。无返回值的函数,其他特性与函数一致
	{
		age = 18;	//在构造函数内部对成员赋值,本质是赋值操作
		gender = 'm';
	}
};
注:1、如果构造函数有参数时,在声明对象时进行传参(无参数则声明对象时不加括号)
      栈对象传参:CStu stu1(arg1, arg2, ...);
      堆对象传参:CStu *stu2 = new CStu(arg1, arg2, ...);
   2、构造函数仍然允许函数重载,根据声明对象时传递的参数匹配符合的构造函数。
   3、构造函数对类成员初始化:
      CStu() : age(18), gender('m')	//成员变量的初始化顺序仅与声明顺序有关,本质是初始化
      {
   	 ...
      }
3.6.2 析构函数

    析构函数在对象生命周期即将结束时执行,形式与构造函数类似,但不能有参数,有且仅有一个,常用于指针成员的内存释放等善后工作。对于指针对象,仅遇到 delete 语句时才会触发析构函数 (free不会触发析构函数) 。

class CStu
{
public:
	int age;
	char *p;
	
	CStu():age(18)	//构造函数
	{
		p = new char;
		...
	}
	
	~CStu()	//析构函数
	{
		delete p;
		...
	}
};
3.6.3 常函数

    常函数内部不可以修改类的数据成员,仅能读取或取类内成员值作运算。但是对于常函数内部的局部变量是可以修改的,常对象仅能调用常函数或几种基础类型的数据成员,构造和析构函数不能作为常函数。
    注:类内常成员只能通过构造函数初始化列表的方式初始化值。

class CStu
{
public:
	int age;
	const int age_const;
	void fun1() const
	{
		cout << "fun1" << endl;
	}
	void fun2()
	{
		cout << "fun2" << endl;
	}
	CStu() : age_const(16)
	{
		age = 18;
	}
};

//main函数如下
	const CStu stu;
	stu.age;	//调用数据成员合法
	stu.fun1(); //常对象调用常函数合法
	// stu.fun2();	//调用非常函数不合法

附:类内静态成员只能在类外初始化,格式如下:
   static int step_add;	//类内声明
   int CStu::step_add = 1;	//类外初始化,类型 类名::变量名 = 值
   
   类的静态成员还可以通过类名作用域的方式调用(多对象共用静态成员):CStu::step_add
   静态常量整形成员可以在声明时初始化,如:
   	static const int step_add = 1;
3.6.4 拷贝构造

    拷贝构造本质是一个特殊的构造函数,不同的是,只有在声明对象并对其初始化时则会调用拷贝构造,而非构造函数。拷贝构造实现的功能主要是使用现有的对象值初始化新声明的对象,如果手动编写拷贝构造,则需手动写入赋值过程(使用传递的参数的各个成员值对当前的成员进行赋值)。

class CStu
{
public:
	int age;
	CStu(const CStu& stu)	//拷贝构造
	{
		this->age = stu.age;	//手动赋值
	}
};

调用拷贝构造的几种情况:
CStu stu1;	//声明最基础的对象,仅调用构造函数
CStu stu2 = stu1;	//声明对象并初始化,调用拷贝构造,下同
CStu stu2 = CStu(stu1);
CStu *stu3 = new CStu(stu1);
注:类对象函数参数时,调用函数时传参给函数,函数会创建局部变量 (对象) ,并用实参的值初始化该局部变量。此情况下也会调用拷贝构造。

深拷贝
    以上谈到的拷贝构造称之为浅拷贝,其存在一些问题。如指针成员,使用浅拷贝时,由于新对象并不经过构造函数 (构造函数为指针成员分配空间) ,所以在指针成员赋值时,新对象并没有为其指针成员申请新的内存空间,而是使用原对象的指针成员对新对象指针成员进行简单的赋值,即两个对象的指针成员实际上指向的是同一数据地址,所以在使用析构函数释放内存时,会存在重复释放这一内存空间的情况,出现非法操作。函数调用过程中的形参也会经过 声明并使用现有的对象(实参)值对其初始化 的步骤。
    因此,对于有指针成员的拷贝构造过程,我们应该手动为指针成员申请内存空间,并进行赋值操作(使用 memcpy 函数),这个过程称之为深拷贝。

class CStu
{
public:
	int *p;
	CStu()
	{
		p = new CStu;
	}	
	CStu(const CStu &stu)	//深拷贝
	{
		this->p = new CStu;	//手动分配内存并赋值
		memcpy(this->p, stu->p, 4);	//需要头文件 cstring
	}
	~CStu()
	{
		delete p;
	}
};
3.6.5 内联函数

    内联函数不同于普通函数的是,它在被“调用”后只是执行简单的替换,即把函数体替换至调用处,成为调用主体的一部分,不产生跳转动作,但不仅限于类内拥有。
    内联函数执行时间略小于普通函数,但是会消耗更多的内存,在大量重复“调用”该函数时尤为明显。内联函数必须使用关键字 inline 表示其是一个内联函数,如果有函数声明,函数声明也必须添加关键字 inline 。
    注:类内函数默认为内联函数,类外的内联函数通常合并至一个头文件中。

inline void fun();	//内联函数声明
...
inline void fun()	//内联函数实体
{
	...
}

3.7 this指针

    在创建对象时,程序则会向成员函数传递一个指向该对象的 this 指针参数,该指针可以在成员函数内指向该对象的成员。
    在类中,我们应该尽量避免函数内局部变量与成员变量重名。如果重名,在局部变量生命周期内,成员变量不可见,即成员变量被局部变量覆盖。this 指针可以指向成员变量,使得在局部内对重名的成员变量仍可见。如下例子:

class CStu
{
public:
	int age;
	CStu(int age)
	{
		age = age;	//语句1
		this->age = age;	//语句2
	}
};
注:默认情况下想要通过传递给构造函数的参数对 age 成员赋值
   使用“语句1”则表示局部变量 age 的值赋给局部变量 age 。
   使用 this 指针如“语句2”,表示局部变量 age 的值赋给成员变量 age 。

3.8 运算符重载

    在某些场景下,系统内部的运算规则已不满足于我们的需求,如两个类对象相加。这在默认情况下是不被允许的,实际上可以使用运算符重载的方法使得看似两个不相关的变量进行运算。但是运算符重载最底层的数据运算必须符合系统默认的运算规则,从某种程度上说运算符重载是一种人为定义的新运算规则,不违背系统的基本运算法则。
    注:不能重定义系统已存在的运算规则,如+运算符重载,参数列表的类型不能都定义为整型、浮点型等。如果运算符重载函数没有返回值,则在连续运算时会出现问题。

3.8.1 基本格式
class CStu
{
public:
	int age;
	char gender;
	CStu():age(18)
	{
	}
};

/*	运算符重载格式如下
返回值类型 operator 运算符 (参数列表)
{
	...	//具体实体,内部不能违背系统基本的运算法则
}
*/

int operator+(CStu& s1, CStu& s2)	//+运算符重载
{
	return s1.age+s2.age;
}

int main(void)
{
	CStu stu1, stu2;
	int add;
	add = stu1 + stu2;	//默认是不允许存在这种运算的
	cout << add << endl;	//应该正确输出36
	return 0;
}
3.8.2 类内运算符重载

    运算符重载在类内时,仅允许双目运算符左侧为该类的对象,同样参数列表仅需写运算符右侧数据类型即可。

class CStu
{
public:
	int age;
	CStu():age(18) {}
	int operator+(CStu& s2)
	{
		return age + s2.age;
	}
};
int main()
{
	CStu stu1, stu2;
	cout << (stu1 + stu2) << endl;
	return 0;
}
3.8.3 输入输出流的运算符重载
// ostream
void operator << (ostream& os, CStu& s)
{
	os << s.age;
}

ostream& operator << (ostream& os, CStu& s)
{
	os << s.age;
	return os;
}


// istream
istream& operator >> (istream& ist, CStu& s)
{
	ist >> s.age;
	return ist;
}
3.8.4 自加运算符重载
// 以下均以对象 stu1 举例,类外重载演示
// 前置++,如 ++stu1,前置--同格式
int operator ++ (CStu& s)
{
	s.age += 1;
	return s.age;
}

// 后置++,如 stu1++,后置--同格式
int operator ++ (CStu& s, int n)
{
	int pre = s.age;
	s.age += 1;
	return pre;
}
3.8.5 类型转换的重载
class CStu
{
public
	int age;
	float weight;
	
	operator int()	// 如使用 (int)stu1 时调用
	{
		return age;
	}
	operator float()	// 如使用 (float)stu1 时调用
	{
		return weight;
	}
};

附:

C++不支持重载的运算符:
	.	.*	::	?:
	
必须作为类成员重载的运算符:
	=	[]	()	->

3.9 多态与虚函数

    多态是指使用父类指针对象可以调用不同子类的函数成员。需要满足的条件是:该函数在父类和子类中都存在,且父类中该函数定义有 virtual 关键字;其次指针分配内存大小时所指明的类型为某个子类。如果有多个子类,则最终调用的是分配空间时所用的类中的函数成员。例如:

class CFather
{
public:
	virtual void fun()
	{
		cout << "CFather" << endl;
	}
};

class CSon1 : public CFather
{
public:
	void fun()
	{
		cout << "CSon1" << endl;
	}
};

class CSon2 : public CFather
{
public:
	void fun()
	{
		cout << "CSon2" << endl;
	}
};

// main 函数
	CFather* p1 = new CSon1;
	p1->fun();    //输出 CSon1

    注意:如上提到,子类和父类含有相同的函数成员,且父类中该函数成员为虚函数,叫做重写。子类中重写的函数(与父类虚函数相同的函数成员),即使未使用 virtual 关键字,默认也是虚函数,即意味着如果子类还有子类,且子子类还有相同函数成员时,它们仍为重写关系。

3.10 虚表

    理解虚表,则先要从之前谈到的虚函数和重写说起。为了让父类(含有虚函数的类)能访问子类函数成员,父类会创建出一个虚表用于存放指向虚函数的指针。每遇到一个虚函数,就会向虚表中写入一个元素(虚函数的地址),当在其子类中遇到重写时,虚表中对应函数的地址就会被子类中的重写函数地址覆盖。
    每声明一个该父类的指针对象,在该对象的空间首 4 个字节(32位编译器)就用于存放虚表的地址。当该指针对象调用重写函数时,就会从虚表中查询函数地址,进而调用函数。其指向关系如下示意图:
在这里插入图片描述

3.11 虚析构

p;   默认情况下,使用子类型对父类型的指针对象分配空间,当指针空间被释放时,只会调用父类的析构函数,使用虚析构函数可以实现父类子类析构函数一并调用(先调用子类再调用父类)。

class CFather
{
public:
    virtual ~CFather()
    {
        cout << "~CFather()" << endl;
    }
};

class CSon : public CFather
{
public:
    virtual ~CSon()
    {
        cout << "~CSon()" << endl;
    }
};

//main 函数
CFather* p1 = new CSon;
delete p1;    

/* 注:释放空间前输出如下:
	~CSon()
	~CFather()
*/

3.12 纯虚函数

    一个类中包含纯虚函数时,该类不能用于实例化对象。必须在其子类中重写,且只能用其子类实例化对象。通常在一个基类中全部使用纯虚函数,并在其子类实现,这样的类称之为接口类,它可以清晰的反应程序的一些结构以及作为一个“标准工厂”。如下:

class CFather
{
public:
	virtual void fun() = 0;	//纯虚函数格式
};

class CSon : 
{
public:
	void fun()
	{
		cout << "Hello!" << endl;
	}
};

//main 函数
CSon c1;
c1.fun();

3.13 内部类

    类内仍可声明一个类,不过它们之间的访问需要特别注意,以下简单示例外部类和内部类的相互访问的方法。(以下用词可能容易造成误解,请以实际代码为准)

3.13.1 外部类访问内部类成员

    在外部类中声明一个内部类的对象,即这个对象是外部类的一个成员。通过外部类对象访问内部类成员时多加一重访问( . 运算符)即可。

class COut
{
	...
public:
	class CIn	//内部类
	{
	public:
		void fun1()
		{
			cout << "in: fun1()" << endl;
		}
	};
	
public:
	CIn in;	//内部类的对象,外部类的成员
};

// main()
COut ou;
ou.in.fun1;	//访问内部类 CIn 的 fun1() 成员
3.13.2 内部类访问外部类的成员

    在内部类定义一个外部类的指针对象,使用构造函数对其初始化指向外部类,随后即可使用该指针对象访问外部类成员(实际实现上步骤已经完成双向访问)。

class COut
{
public:
	void fun1()
	{
		cout << "COut: fun1()" << endl;
	}

	class CIn	//内部类
	{
	public:
		COut* po;    //外部类指针对象
		void fun2()
		{
			po->fun1();
		}
		CIn(COut* pf) : po(pf)	//对外部类指针对象初始化
		{
		}
	};

public:
	CIn in;	//内部类对象,为外部类成员
	COut() : in(this)	//外部类构造函数,对in成员初始化为指向自己的指针
	{
	}
};

// main()
COut ou;
ou.in.fun2();	//输出:COut: fun1()
/* 说明:此处效果等同于ou.fun1()
   通过外部类对象 ou 访问其 成员in,再通过 成员in 访问到 外部类成员 fun1(),
   绕了一个圈子,双向访问的典型
*/


4 模板

4.1 函数模板

    以之前提到的函数重载为例,如果有多个类型需要实现重载,我们就需要写出多个函数实体,未免有些冗余。如果使用函数模板,就可以大大较少代码量,用一套方案实现不同的功能,这也是泛型编程思想的表现之一。

template<typename P_TYPE>
void fun(P_TYPE p)
{
	cout << p << endl;
}

// main()
fun(12);
fun(12.3f);
fun('a');
//输出12,12.3,a

    如上代码,模板声明一个通用类型 P_TYPE,根据传入的实参类型自动转换,并进行输出。注:模板作用域仅限后面紧跟的代码段。如果传入的为结构i体或者类对象,函数内应该使用对应对象内的成员。

4.2 类模板
template<typename T = int>	//默认类型为int,仅类模板支持默认类型
class CNode
{
public:
	T age;
	CNode(T p):age(p)
	{
	}
};

//main()
CStu<char> stu1('a');	// 传入char类型
CStu<> stu2(18);	//不传如类型,默认为前面指明的 int 类型
cout << stu1.age << " " << stu2.age << endl;	// 输出:a 18
CStu<int>* ps = new CStu<int>;	//指针对象声明格式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值