C++学习笔记(基本知识+封装)

观看慕课网《C++远征》系列课程的笔记,链接https://www.imooc.com/u/1349694/courses?sort=publish

基本知识点

引用与指针

1、引用就是一种“别名”,变量不能只有“别名”
2、引用必须初始化

int a=10;
int &b; //是错误的,赋值成nullprt也不行
int &b=a; //b有初始化,正确

3、结构体的引用(略,没什么坑)
4、指针类型的应用

int a=10;
int *p = &a;
int *&q = p; //q是指针p的引用,
*q = 10; //对q的操作等价于等p的操作

5、函数参数的引用,引用参数可以完成指针参数的功能,且不需要额外的空间,写起来也少很多符号

const

1、const与指针类型的关系
遵循就近原则
const后面跟着*p,就是修饰p所指向的内容,*p的值不可修改,即指向常量的指针
const后面跟着p,就是修饰p本身,p的值不可修改,即指针本身是一个常量
变量类型与const的相对位置关系并不影响

const int *p=nullptr;
int const *p=nullptr;

const int * const p =nullptr; //指针是常量,所指的也是常量
int const * const p = nullptr;

int* const p;

一种错误

const int x=3; //x不可修改
int *y=&x; //*y没有指定不可修改,那就有修改常量x的风险,会报错
const int *y=&x; //正确

int x=3;
const int *y=&x; //没有问题

2、const与引用

int x=3;
const int &y = x;
x=10; //正确,x本身并不是一个常量
y=20; //错误,y是x的引用且是常量,不可以修改,在函数参量中经常使用

const int
函数特性

1、有默认参数值的参数必须在参数表的最右端
2、声明的时候可以写默认值,定义的时候不建议写,有些编译器不能通过
3、无实参则用默认值,否者实参覆盖默认值
4、函数重载

在同一作用域上
函数同名,但是参数个数和参数类型不同
编译器在进行编译时会将函数名加上各个参数类型重名为一个新函数名,所以实际上各个重载函数都有不一样的名字

5、内联函数
在调用内敛函数时,内联函数的内容在编译时自动复制到调用处,所以不用进入一般函数调用时的出栈入栈动作,可以提升效率,但是也有以下问题

内联编译是建议性的,由编译器最终决定
逻辑简单且频繁调用的函数建议使用内联
递归函数无法使用内联方式(内联函数本质上已经不算是一个函数,是编译器在进行ctrl+c and ctrl+v)

内存管理

1、内存的掌管者是操作系统,用户进行申请和释放
2、new和delete是运算符而不是函数
3、new和delete配套,malloc和free配套(C语言特性)
4、如有必要要注意判断申请是否成功

int *p = new int[1000];
if(p==nullptr){
	//分配失败
	//异常处理
}
delete []p;
p=nullptr; //一定要置为空,否则会成为野指针,后面可能会造成一些莫名其妙的错误

5、内存分配时可以顺便初始化

int *p=new int(20); //赋值为20

6、分配一大片内存时,释放时注意写上[],否则只会释放内存的第一个空间

int *p = new int[10];
delete p; //只有p[0]被释放
delete []p; //正确
p=nullptr;

封装

类与对象

类对象的实例化有两种方式,一种是在栈区,一种是在堆区

class A{
public:
	int x=0;
	int print(){
		cout<<"this is A"<<endl;
	}
}
int main(){
	A a1;//在栈区实例化
	a1.print()
	A *a2 = new A(); //在堆区实例化
	a->print();
	//类数组
	A a3[5];
	A *a4 = new A[5];
	//类数组的坑!!
	a3->print(); //正确,a是一个指针,指向数组的第一位,用->来访问成员
	a3[0]->print(); //错误,尽管也是在对数组的第一位进行操作,但是a3[0]不是指针,要用 . 来访问成员
	(a3+1)->print(); //正确,a3+1跟a3一样,是一个指针,所以用->

}
封装的意义

类和对象就是把数据和对数据的处理封装到一起,面向对象的基本思想是以对象为中心,将所有数据操作转化成员函数的调用,包括赋值

类内定义与类外定义

1、类内定义的函数一般编译器会优先将其当做内联函数
2、类外定义
同文件类外定义

// Car.cpp
class Car{
public:
	void run();
}

Car::stop(){
	//...
}

分文件类外定义
c++项目一般都采用这种方式,头文件中声明,源文件中定义

//Car.h
class Car{
public:
	void run();
	void stop();
}

//Car.cpp
#include "Car.h"
void Car::stop(){
	//...
}


构造函数和析构函数

课程中的一张图,同一个类多个对象的成员变量是各自存储的,但是成员函数是共享的,从代码区的同一个位置读取
1、构造函数在对象实例化时自动调用
2、构造函数与类名同名,没有返回值,void也不用写,可以重载,但在实例化时只会运行其中一个
3、用户没有定义构造函数,编译器会自动生成一个,但函数内部什么都不做
4、拷贝构造函数

class Student{
public:
	Student(){
		cout<<"student";
	}
}

int main(){
	Student stu1;
	Student stu2=stu1; //不会打印
	Student stu3(stu1); //不会打印
	//程序运行结束只会打印一次student
}

对象实例化时是会自动运行构造函数的,但是如果是通过直接复制或者复制初始化实例化对象时,是去自动调用拷贝函数,拷贝构造函数在用户没有定义时,编译器同样会自动生成。
拷贝构造函数的定义只有一种固定形式,如下
类名(const 类名& 变量名)

class Student{
public:
	Student(){
		cout<<"student";
	}
	Student(const Student& stu){}
}

5、析构函数没有参数,不能重载,编译器也会自动生成
6、析构函数在对象销毁时自动调用

对象的生命历程
申请内存》初始化列表》构造函数》参与运算》析构函数》释放内存

初始化列表

1、实例化对象时不需要传入参数的构造函数称为默认构造函数,有两种形式

class Student{
public:
	Student(){} //形式1,什么都没有
	Student(string m_name="Jim") //有参数,但是参数有默认值
private:
	string m_name;
}
2、初始化列表的形式,函数名括号后加个帽号
```cpp
class Student{
public:
	Student():m_name("Jim"),i_age(10){ }
private:
	string m_name;
	string i_age;
}

3、初始化列表先于构造函数执行
4、初始化列表只能用于构造函数
5、可以同时初始化多个成员,速度很快,推荐使用
6、初始化列表可以去取构造函数的传入参数

class Student{
public:
	Student(string name, string age):m_name(name),i_age(age){ }
private:
	string m_name;
	string i_age;
}

注意const型变量初始化的坑

class Student{
public:
	Student():i_age(10){ } //正确
	Student(){
		i_age=10;		//错误,构造函数内对const变量赋值属于二次赋值,编译无法通过
	}
private:
	const int i_age;
	
}
对象成员

1、如果对象成员是另一类的对象时,那么在初始化时是先调用对象成员的构造函数,再调用自己这个类的构造函数,析构则相反,即先初始化零部件,再初始化自己。
2、如果对象A中有对象成员B,对象B没有默认构造函数,那么对象A必须在初始化列表中初始化对象B。

class Coordinate{
public:
	Coordinate(int x, int y){ //构造函数有参数,但是参数没有默认值,不是默认构造函数
		m_X = x;
		m_Y = y;
	}
	~Coordinate(){ }
	void disp(){
		cout<<m_X<<""<<m_Y<<endl;
	}
	

private:
	int m_X;
	int m_Y;
};

class Line {
public:
	//初始化列表中对两个成员变量进行了初始化,初始化的方式是调用了它们的构造函数
	Line(int x1,int y1, int x2, int y2):m_coorA(x1,y1),m_coorB(x2,y2){ } 

	//错误写法
	Line(int x1,int y1, int x2, int y2){
		m_coorA(x1,y1); 
		m_coorB(x2,y2);
	}


	~Line(){ }
private:
	Coordinate m_coorA; //声明
	Coordinate m_coorB;
};
深拷贝与浅拷贝

如果类成员变量中没有指针类的变量,那么深拷贝与浅拷贝没有太多区别,如果有指针变量,则只能用深拷贝,浅拷贝会造成程序崩溃

class Array{
public:
	Array(){
		m_iCount=5;
		m_pArr = new int[m_icount];
	}
	Array(const Array& arr){
		m_iCount = arr.m_iCount; //用arr的m_iCount对自己的m_iCount进行赋值,两个m_iCount具有不同的地址
		m_pArr = arr.m_pArr; //用arr的m_pArr对自己的m_pArr进行赋值,两个m_pArr具有不同的地址
	}
	~Array(){
		delete []m_pArr;
		m_pArr = nullptr;
	}

private:
	int m_iCount;
	int *m_pArr;
}

分析:上面实现了浅拷贝,用arr的m_iCount对自己的m_iCount进行赋值,两个m_iCount具有不同的地址,两个对象在销毁的时候不会有任何问题。用arr的m_pArr对自己的m_pArr进行赋值,两个m_pArr也是具有不同的地址,问题是他们指向了同一片内存区域,那么当其中一个对象销毁时,这片内存区域会被释放,那么当另一个对象也进行销毁时,它指向的那片区域已经被释放了,此时程序就会崩溃。
所以当存在指针型的变量时,如果要进行对象拷贝,需要进行深拷贝,即在新对象中要申请一块新的内存区域,然后对内存区域内的值逐一拷贝,即

class Array{
public:
	Array(){
		m_iCount=5;
		m_pArr = new int[m_iCount];
	}
	Array(const Array& arr){
		m_iCount = arr.m_iCount; 
		m_pArr = new int[m_iCount];  //申请新内存
		for(int i=0;i<m_iCount;++i)		//逐一拷贝
			m_pArr[i] = arr.m_pArr[i];
	}
	~Array(){
		delete []m_pArr;
		m_pArr = nullptr;
	}

private:
	int m_iCount;
	int *m_pArr;
}
对象指针与对象成员指针

1、对象指针在前面已经用过很多次了,略
2、对象成员指针是指对象的成员是一个指针,在上文中也提到这种情况下拷贝要用深拷贝
3、对象成员指针所指向的区域的内存大小不计入对象内

class Line {
public:
	//初始化列表中对两个成员变量进行了初始化,初始化的方式是调用了它们的构造函数
	Line(int x1,int y1, int x2, int y2):m_coorA(x1,y1),m_coorB(x2,y2){ } 
	~Line(){ 
		delete m_coorA;
		delete m_coorB;
		m_coorA = nullptr;
		m_coorB = nullptr;
	}
private:
	Coordinate *m_coorA; //声明
	Coordinate *m_coorB;
};

int main(){

	Line line(1,2,3,4);
	cout<<sizeof(line)<<endl; //输出8
}

分析:Coordinate类中有两个int型变量,所以这个类的对象应该具有八个字节,如果Line中有两个Coordinate对象,则对象line应该具有16字节,然而现在Line中是有两个Coordinate对象指针,32位系统中任何指针都是4个字节的,所以line对象具有8字节。因此,对象成员指针所指的区域不计入对象内。
这种情况下,在销毁line对象时,是先调用coordinate的析构函数(按照声明的顺序),然后再去调用line自己的析构函数。如果不是用对象指针直接用对象,则是先调用line的析构,再调用coordinate的析构。在定义时,构造函数的调用顺序则是相同的,先调用coordinate,在调用line。

this指针

1、指向对象自己的指针

Array arr1  // this 等价于 &arr1

2、成员函数的输出参数中默认存在一个this指针,由编译器自动加入,该指针处于参数列表的第一位
3、this指针可以作为返回值,但是这里要注意一个坑。
4、第3点中,如果不是返回this,而是函数内部定义的局部变量,那么不可以返回该局部变量的指针或者引用,因为一旦退出函数,该变量就会销毁,而this指针是一直伴随的对象存在的,只要对象不被销毁。

class Student{
public:
	Student(int age){
		i_age = age;
	}
	~Student(){ }
	Student(const Student& stu){
		cout<<"copying"<<endl;
	}
	Student* getThis(); //三种获取this的方式
	Student& getThis();
	Studetn getthis();
private:
	int i_age;
}

Student:: Student getthis(){
	return this; 		//返回this指针,在外部该指针就等价于该对象
}
Student:: Student& getthis(){
	return this;		//返回引用,在外部该引用也等价于该对象
}
Student:: Student getthis(){
	return *this;	//返回一个对象的副本!!如果运行代码可以发现会调用拷贝构造函数,在外部它跟本对象是相互独立的
}
const的作用

1、const类型的成员变量只能在初始化列表中进行赋值,这种变量也叫常成员变量

class Coordinate{
public:
	Coordinate(int x, int y){ //构造函数有参数,但是参数没有默认值,不是默认构造函数
		m_X = x;
		m_Y = y;
	}
	void setX(int x){ m_X = x; }
	void setY(int y){ m_Y = y; }
	int getX(){return m_X;}
	int getY(){return m_Y;}
	~Coordinate(){ }
private:
	int m_X;
	int m_Y;
};
class Line {
public:
	//初始化列表中对两个成员变量进行了初始化,初始化的方式是调用了它们的构造函数
	Line(int x1,int y1, int x2, int y2):m_coorA(x1,y1),m_coorB(x2,y2){ } 
	void setA(int x, int y){
		m_coorA.setX(x);		//报错点
		m_coorA.setY(y);		//报错点
	}
	void setB(int x, int y){
		m_coorB.setX(x);
		m_coorB.setY(y);
	}
	void printInfo(){
		cout<<m_coorA.getX()<<" "<<m_coorA.getY()<<endl; //报错点
		cout<<m_coorb.getX()<<" "<<m_coorB.getY()<<endl; //不会报错
	}
	~Line(){ }
private:
	const Coordinate m_coorA;   //声明成常成员
	Coordinate m_coorB;		//普通成员
};

int main(){
	Line line(1,2,3,4);
	line.setA(10,20); //会报错,因为m_coorA是常成员变量,不能再赋值,不能再初始化列表中赋值
					//const成员变量相当于只有读权限的变量,不允许以任何方式做修改

	

}

2、常成员函数

//类定义与上方相同
int main(){
	line.printInfo() //同样会报错!!即使没有去做修改,但是m_coorA是Coordinate类的常对象,它只能调用常成员函数,
						//而getX()和getY()都是普通成员函数。
}

1、如果没有加const修饰,编译器为默认为成员函数添加一个this指针,比如下方。

int getX(){return m_X;}
//等价于
int getX(Coordinate* this){return m_X;}

2、常对象是不允许去调用上方这种函数的,即使在函数中没有修改任何变量,常对象只能调用常成员函数,定义方式如下方,const放在函数名后,必须是后面,不能放在函数名前面。写成等价方式也可以

int getX() const {return m_X;}
//等价于
int getX(const Coordinate* this){return m_X;}

3、常成员函数与普通成员函数同名时互为重载,普通对象会去调用普通成员函数,常对象会去调用常成员函数

void printInfo(){
	cout<<m_coorA.getX()<<" "<<m_coorA.getY()<<endl; 
	cout<<m_coorb.getX()<<" "<<m_coorB.getY()<<endl; 
}

void printInfo() const{
	cout<<m_coorA.getX()<<" "<<m_coorA.getY()<<endl; 
	cout<<m_coorb.getX()<<" "<<m_coorB.getY()<<endl; 
}

4、普通对象可以去调用所有的成员函数,不管成员函数有没有const修饰。

常指针和常引用
class Coordinate{
public:
	Coordinate(int x, int y){ //构造函数有参数,但是参数没有默认值,不是默认构造函数
		m_X = x;
		m_Y = y;
	}
	int getX(){return m_X;}
	int getY(){return m_Y;}
	void printInfo() const; //常成员函数
	~Coordinate(){ }
private:
	int m_X;
	int m_Y;
};
int main(){
	Coordinate coor1(3,5); //定义一个普通对象
	const Coordinate &coor2 = coor1; //coor2是对象的常引用
	const Coordinate *pCoor = &coor1; //pCoor是一个指向常对象的指针
	Coordinate const * pCoor2 = &coor1; //pCoor2是一个常指针,指向一个普通对象
	
	//调用常成员函数,都可以
	coor1.printInfo(); //正确,普通对象可以调用常成员函数
	coor2.printInfo(); //正确
	pCoor->printInfo(); //正确
	pCoor2->printInfo(); //正确
	
	//调用普通成员函数
	coor1.getX(); //正确
	coor2.getX(); //错误,常引用只能调用常成员函数
	pCoor->getX(); //错误, 常对象的指针也只能调用常成员函数
	pCoor2->getX();//正确,普通对象的指针什么都能调用
	
	//修改指针指向的位置
	Coordinate coorB(7,9);
	pCoor1 = &coorB //pCoor1指针本身不是常变量,它可以指向别的对象
	pCoor2 = &coorB //pCoor2是一个常指针,不能再指向别的对象

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值