前言
在b站上跟随黑马程序员的视频学习C++,经过几个月总算是将初步阶段的视频给看完了,这篇文章就是总结这一段时间来学到的知识点以及自己的一些感受和领悟。虽然只是小小一部分,离该有的水平还差很多,但是慢慢来吧,加油!!!!
一、知识点
关于C++的知识点,目前学到的只是一小部分。
前期学习包括C语言中的点:变量,常量,关键字,数据类型,标识符,运算符,程序结构,数组,函数,指针,结构体。
程序在内存中的内存模型:代码区,全局区,堆区,栈区。
C++对于C语言的扩充:引用,函数参数,占位参数,函数重载,以及三大特性:封装、继承、多态。同时了解构造函数和析构函数,类,对象,文件操作。
(一)C语言基础知识点
变量
本质就是向内存申请空间,变量名就是为这块空间取一个别名,方便程序员对这块空间进行操作。
常量
就是不能被程序员修改的值,对常量修改是不被允许的。
关键字
就是系统提前定义并赋予特殊意义的标识符,关键字不能作为变量名被程序员使用,其实就是无法对其进行重载,重新赋予新的意义。
数据类型
分为系统定义和程序员自定义,系统给出的数据类型比如int char string double等等,也属于关键字。程序员自定义的数据类型可以理解为自己定义的结构体和类,通过结构体和类来实例化对象,也相当于是用数据类型定义了一个变量,只是实例化的对象一般开辟在堆区,由程序员开辟和释放,也会在程序执行结束时由系统自动释放(最好养成手动开辟手动释放的习惯)。
(类和对象以及字符串数据类型等属于C++知识,此做扩充说明。)
整型:int、long、longlong
字符型:char
字符串型:string
浮点型:float(单精度) 、 double(双精度)
布尔型:true、false
标识符
是程序员用来表示某一个实体的特殊符号或者字符串,例如变量名等。
运算符
包括算术运算符,逻辑运算符,关系运算符,赋值运算符,位运算符等。
1、算术运算符:+、-、*、/、%(模运算,即取余数)
2、逻辑运算符:|| (逻辑或)、&& (逻辑与)
3、关系运算符:>、<、=、>=、<=、==、!= (不等于),!表示非
4、赋值运算符:=
5、位运算符:>> (右移运算符)、<< (左移运算符)、| (按位或)、& (按位与)
程序结构
选择结构
if(判断条件)...{ }...else...{ }...(可嵌套使用,判断条件也可叠加,使用&&、|| 来叠加条件)
多分支结构
switch(判断条件)...{ }... 提供多分支入口,实现不同条件进行选择
循环结构
1、while(判断条件)...{ }...判断条件为true 、false,使用时需要慎重思考停止条件,否则容易死循环。
2、for(设置判断变量;判断条件;使判断变量变化条件)...{ }...使用非常灵活,可嵌套使用。
3、do...while(判断条件)...{ }...不管判断条件是什么,都先执行一次循环体内语句,在进行判断,与while循环类似。
跳转语句
1、break ...用于跳出循环体,尤其switch中使用较多。
2、continue ...用于让系统执行时跳过本次continue之后的语句,如果有下一次执行时会执行后续语句,一般用在循环体内。
3、goto...跳转到设定的位置。设定一个标识符,在需要跳转到的位置前设置该标识符,在需要跳转的位置写goto 标识符,在程序执行时即可跳转到标识符存在的位置。慎用,如果使用不当,极容易导致程序执行时出现执行逻辑错误。
数组
有一维数组、二维数组。可定义为整型数组,字符数组,指针数组等等。
数组应用范围很广且灵活,定义方式为:
一维数组:数据类型 数组名[ 数组内元素个数 ]={ 数组元素 }...
二维数组:数据类型 数组名 [ 数组元素个数 ] [ 数组元素个数 ]={ 数组元素;数组元素 }...利用;(分号)来分割数据维度。
函数
编写函数规范
返回类型 函数名( 参数 )...{ 函数体 }...
功能
封装一个实现特定功能的函数,然后在需要时去调用。主要是为了减少重复代码,提高代码的复用性。
调用
在代码中,直接写出函数名(),即可。如果函数中定义了参数,则在调用时需要传参,如果函数中设定了返回类型,拥有返回值,则需要定义一个变量来接收返回值。
函数调用传参
1、传值调用:直接将定义并赋了值的变量传递给函数,函数会重新申请空间来存放传来的值,导致无法真正改变变量的值,只在函数的作用域下改变了变量的值,一旦出了函数的作用域,该变量会马上被释放,原来的变量的值并未发生改变。函数参数设置中定义新的变量来接收传来的值。
即形参改变不影响实参的值。
函数定义形式:void test( int a , int b )...{ 函数体 }...
//交换函数
void test(int a , int b)
{
int temp=0;
temp=a;
a=b;
b=temp;
}
int main()
{
int a=1;
int b=2;
test(a,b);
return 0;
}
2、传址调用:将定义并赋值的变量的地址传递给函数,函数会根据传来的地址在内存中找到这块空间,对里面的值进行操作。可以真正改变原变量的值,达到程序员向对变量的值进行操作的目的。相应的在函数的参数设置中应该定义指针来接收传来的变量的地址。
形参的改变会影响实参的值。
函数定义形式:void test(int *a , int *b)...{ 函数体 }...
//交换函数
void test(int *a , int *b)
{
int temp=0;
temp=a;
a=b;
b=temp;
}
int main()
{
int a=1;
int b=2;
test(&a,&b);
return 0;
}
3、引用调用:与传址调用类似,只在函数定义时有区别。在函数参数定义时,改为引用,用&符号。引用就是给变量取一个别名,别名和变量名指向的是同一块空间,同样可以对空间内的值进行操作。调用时传过去的也是原变量的地址,用引用来接收。
形参的改变会影响实参的值。
函数定义形式:void test(int &a , int &b)...{ 函数体 }...
//交换函数
void test(int &a , int &b)
{
int temp=0;
temp=a;
a=b;
b=temp;
}
int main()
{
int a=1;
int b=2;
test(a,b);
return 0;
}
函数声明
返回类型 函数名 ();
void protect();
在项目中一般会有很多文件,在不同的文件内编写不同的代码,所以需要先对要用的函数声明,可以先不写函数的定义,告诉编译器我的代码中有这个函数。如果函数定义在函数调用的后面,编译器就会报错,找不到该函数,因为C++程序是一行一行编译的,语句有先后。
指针
指针就是内存空间的地址,指针变量就是存放这个地址的空间。指针可以指向一块空间,也可以利用指针对这块空间内的数据进行操作。这需要用到解引用,使用 * 符号。
形式:
int *p=&a;
这就是把变量 a 的地址取到然后存放到指针变量 p 中,p 是地址,而 *p 就是这个地址指向的空间内的数据,*p 就是对 p 解引用,通过解引用就可以对值进行操作。
野指针
在指针的应用中,一个指针应该要指向一块空间,但是在一段程序执行结束后,指针指向的空间会被释放,这时就会出现问题,指针指向的空间已经不是这段程序里要操作的空间,已经无权访问。指针乱指,就被称为野指针。
int a=1;
int *p=&a;
delete p;
空指针
由野指针可知,为了不对原有数据造成影响,每次程序结束的时候都要将指针置空,即让这个指针指向内存中的0号位置,由于这个空间是由操作系统控制的,用户无法访问,指向它不会对数据造成影响,又不会让指针乱指。这就是空指针,简单说就是将指针置空。
形式:
int *p=NULL;
const 修饰的指针
const 是修饰常量的关键字。const 修饰指针时分为三种:
1、常量指针:指针的指向可以更改,但是指针指向的值不能改变。const 修饰的是整个指针 *p,而 *p 解引用表示指向的值。
const int *p;
2、指针常量:指针的指向不能改变,但是指针指向的值可以改变。const 修饰的是指针 p ,p 表示地址。
int * const p;
3、指针的指向以及指针指向的值都不可以改变。const 既修饰 p,也修饰 *p。
const int * const p;
指针与数组
数组名 arr 就表示数组的地址,也可以理解为数组第一个元素的地址,即 &arr[ 0 ] 等价于 &arr
而&arr[ 1 ] 则表示取数组中下标为 1 的元素的地址。
形式:
int arr[3]={1,2,3};
int *p=&arr;
int *p=&arr[0];
指针与函数
如函数中所表示,在函数定义时参数就可以使用指针来定义,在函数调用时就需要传递值的地址。
结构体
是由一些数据整合组成的结构性数据集合。一般是定义一些共同属性,由这个结构体定义的变量都会具有这些属性。与C++中的 类 相似。
struct Student
{
string name;
int age;
int num;
};
结构体数组
利用结构体来定义一个数组,将结构体中的属性作为数组的一个元素,存放到数组中。可以定义多的变量,并给这些变量的属性赋值。然后这些值就存放在数组中。
//定义结构体
struct Student
{
string name;
int age;
int num;
};
int main()
{
//结构体数组
struct Student arr[3]=
{
{"张三",18,1},
{"李四",18,2},
{"王麻",18,3},
}
return0;
}
结构体指针
将结构体变量取地址,在定义一个该结构体的指针,将结构体对象的地址存放到指针中。
可以利用 -> 操作符,用指针对该结构体对象的属性进行操作。
struct Student
{
string name;
int age;
int num;
};
int main()
{
//初始化结构体对象,并取地址存到指针
struct Student stu = { "张三",18,1 };
struct Student* p = &stu;
//通过指针和 -> 操作符对对象的属性进行操作
p->name = "李四";
p->age=99;
return 0;
}
结构体做函数参数
跟之前的传参是一样的意思。
1、值传递--形参的值改变不会影响到实参的值
struct Student
{
string name;
int age;
int num;
};
void test(struct Student stu)
{
//在函数内将属性值修改--不会对原本的属性值产生影响
stu.name="李四";
stu.age=20;
stu.num=2;
}
int main()
{
struct Student stu;
stu.name="张三";
stu.age=18;
stu.num=1;
test(stu);
return 0;
}
2、地址传递--形参的改变会影响实参的值
struct Student
{
string name;
int age;
int num;
};
void test(struct Student *stu)//传址调用,用指针接收地址
{
//在函数内将属性值修改--会对原本的属性值产生影响
stu.name="李四";
stu.age=20;
stu.num=2;
}
int main()
{
struct Student stu;
stu.name="张三";
stu.age=18;
stu.num=1;
test(&stu);
return 0;
}
const修饰的结构体
为了防止用户误操作,将不该修改的属性值无意中修改,因此加上 const 修饰来让特定的属性值无法修改。
(二)、C++知识点--对于C语言的扩展
new运算符
程序员利用new运算符在堆区开辟空间。
引用
就是给变量取别名。使得别名指向的空间与变量指向的空间是同一块,大概就是这个意思。前面函数参数的引用调用就是这道理。
注意事项:引用一定要初始化,并且初始化之后就不可以更改。
引用作为函数返回值
但是不能返回局部变量的引用。因为局部变量是开辟在栈区,当函数运行结束后会被释放。如果返回局部变量的引用,就会出现问题,值会错误。
int& test01()//引用的方式返回值
{
int a=1;//局部变量,在函数执行结束后释放
return a;
}
int& test02()
{
static int b=2;//b 为全局变量,开辟在全局区,在整个程序执行完后释放
return b;
}
int main()
{
int a=1;
int& b=a;//引用 b,给 a 取别名为 b;此时引用一定要初始化,且之后不能更改
int c=2;
//b=c;//这是错误的,这是赋值操作,而不是更改引用的指向
int& ref=test01();//用引用来接收函数的返回值,但是会出错,因为返回的是局部变量的引用
test02()=100;//引用作为返回值可以作为左值,并可以给这个引用重新赋值,要求被引用的变量为全局变量
}
引用的本质
为指针常量。当定义一个引用时,编译器会在编译时自动将其转换成指针常量。
int a=1;
int& b=a; int * const b=a;//这两个是等价的,const 修饰的是指针 b
//就说明引用的指向不可更改
常量引用
用 const 来修饰引用,引用在使用时必须引用一块合法的内存空间,不能引用一个常量,但是在const 修饰后,就可以引用一个常量。
int a=10;
int& b=a;//合法
int& b=10;//错误的
const int& b=10;//正确的
//此时这条语句会被转化为 int temp=10; int& b=temp;此时就是合法的
函数的参数
函数的默认参数
在函数声明时,可以对函数的参数进行初始化,这就是给参数一个默认值。
要注意的是,如果一个位置的参数有了默认值,那么这个位置之后的所有参数都需要赋默认值。并且,如果在声明中给了默认值,在函数定义中就不能再次赋默认值。
void test(int a=1,int b=2,int c=3);
函数的占位参数
利用数据类型关键字,可以在函数的定义时进行占位操作。要注意的是,在函数调用时一定要给占位参数传值。初级阶段占位参数不怎么用得上。
void test(int a=1,int);//第二个参数为占位参数
函数重载
就是使用同一个函数名,定义不同功能的函数。主要是利用参数的不同,例如参数个数的不同,参数数据类型的不同,就可以给函数进行重载。注意:如果参数在声明中给了默认值,可能会产生冲突。像例所示,如果在函数调用时传递两个参数,就会导致第一个重载跟第二个重载产生冲突。
void test(int a,int b,int c=2);//参数 c 给了一个默认值
void test(int a,int b);
void test(string a,int b);
类和对象
类的定义
利用 class 关键字可以定义一个类。
class 和 struct 的区别就是默认权限的不同。class 的默认权限是私有的,struct 的默认权限是公有的。
class People{ };
类内的变量叫做成员变量,也叫成员属性,类内的函数叫做成员方法。
成员变量的访问权限有三种:pubilc 、private 、protected。
public:公共访问权限,类内类外都可访问。
private:私有访问权限,类内可以访问,类外不可访问。
protected:保护访问权限,类内可以访问,类外不可访问,但是子类和友元类可以访问。
类对象也可以作为另外一个类的成员。
class shoes
{
};
class Person
{
public:
shoes sho;//一个类的对象作为另一个类的成员
};
对象定义
就是通过类实例化出的对象模型。是实际存在的东西。
Person p;//在栈区开辟一个对象空间
Person *p=new Person;//在堆区开辟一个对象空间--利用指针去维护他
类内的默认函数
构造和析构
类内有两个默认的函数方法,一个是构造函数,一个是析构函数。
构造函数是对类内成员属性进行初始化的函数。
析构函数是程序执行完成后,系统释放对象内存空间的函数。
二者的函数名与类名相同,析构函数函数名前要加一个 ~ 。
构造函数分为默认构造函数、有参构造函数和拷贝构造函数。
默认构造函数由编译器自动给出,是没有参数且没有函数体的空函数。
有参构造需要程序员编写,为函数添加参数和函数体。
拷贝构造也需要程序员编写,属于有参构造的一种,传入的参数为另外一个对象,通常也会用 const 来修饰,并且用引用的方式来传入。作用就是将一个对象的所有属性都拷贝到新的对象上。
class Person
{
public:
Person()//无参构造函数(默认构造)
{
}
Person(int a)//有参构造函数
{
this.a=a;
}
Person(const Person &p)//拷贝构造函数--传入对象 p,将 p 的属性 a 的值拷贝到新对象
{
this.a=p.a;
}
~Person()//析构函数
{
}
int a;
};
构造函数的调用方式
1、括号法
注意:调用无参构造时,不要加()--否则会被编译器识别为一个函数声明。
int main()
{
Person p1;//无参构造函数调用--调用无参构造时,不要加()
Person p2(a);//有参构成调用--传入一个变量 a
Person p3(p2);//拷贝构造调用--传入一个对象 p2,将属性拷贝到一个新对象 p3
return 0;
}
2、显示法
注意:
1、如果把等号右边的 Person(a) 或者 Person(p2),单独拿出来,会被编译器识别为一个匿名对象,即没有名字的对象,特点就是在该行执行结束后会被立即释放掉,因为没有对象名,后面也无法去使用这个对象。因此这里的显示法就是在等号左边定义一个对象名,相当于给右边这块匿名对象取了一个名字,这样后面就可以去使用这个对象,而不会在该行结束后立即被释放。
2、不要利用拷贝构造去初始化一个匿名对象,编译器会报错:重定义。比如 Person (p2),编译器会认为 Person (p2) === Person p2;此时的()没有意义,并且此时 p2 已经在前面定义过了,因此会被认为重定义。
int main()
{
Person p1;
Person p2 = Person(a);//有参构造
Person p3 = Person(p2);//拷贝构造
return 0;
}
3、隐式转换法
编译器会自动将格式转换成显示法的格式。
int main()
{
Person p1;
Person p2 = a;//有参构造--转换成 Person p2 = Person(a)
Person p3 = p2;//拷贝构造--转换成 Person p3 = Person(p2)
return 0;
}
三种方式中,我认为括号法最为简便和方便使用和理解。看个人喜好和要求来具体决定。
构造函数的调用规则
1、当程序员没有定义任何构造和析构函数时,编译器会默认给出三个函数:默认构造、默认析构、默认拷贝函数。
2、当程序员定义了有参构造时,系统不再给出默认构造,但会给出默认析默认拷贝构造函数。
3、当程序员定义了拷贝构造时,系统不再给出构造函数,但会给出默认析构函数。
构造函数的调用顺序
在具有继承关系的类之间,一般是先调用父类的构造函数,在调用子类的构造函数。最后析构的时候,先调用子类的析构函数,再调用父类的析构函数。也就是说,构造函数和析构函数的调用顺序的刚好相反的。
拷贝构造的调用时机
1、利用一个已经创建完成的对象来初始化一个新对象
将对象的所有属性都拷贝一份赋到新对象上,用来初始化新对象。
int main()
{
Person p1(10);//创建一个对象 p1,并且传值 10
Person p2(p1);//利用已经创建好的对象 p1 来初始化新对象 p2
return 0;
}
2、值传递的方式给函数参数传值
在函数中会调用拷贝构造来创建临时对象。
void done(Person p)//此时会调用 Person 的拷贝构造创建一个临时对象来接收传来的值
{
}
int main()
{
Person p;
done(p);//调用 done() 函数,将对象 p 以值传递的方式传过去
return 0;
}
3、值方式返回局部对象
在函数中创建一个对象,属于局部对象,会在函数结束后被释放,所以系统会创建一个临时副本来存放局部对象的值,然后将其返回。
Person done()
{
Person p1;
return p1;//此时会创建一个新的副本,将 p1 的值赋给副本后,将副本返回回去
}
int main()
{
Person p = done();//调用 done 函数,然后回返回一个Person 类型的返回值 p
//此时函数内的 p1 和这里的 p,不是同一个对象,不在同一个地址下
return 0;
}
深拷贝和浅拷贝
拷贝构造分为深拷贝和浅拷贝。
浅拷贝是定义一个新的变量,将一个对象的属性拷贝到这个新变量中,但是这个新变量指向的空间和原来对象指向的空间是同一块,本质是二者共享同一块数据。这样就会导致一个问题,在释放堆区这块数据的时候,新对象释放一次,然后原来的对象又会去释放这块空间,就会导致非法操作重复释放。系统提供的默认拷贝构造就是浅拷贝操作。
深拷贝就是在堆区重新开辟一块空间,然后将一个对象的属性全部拷贝到这个新空间中。这样的话在释放堆区数据的时候,两个对象释放的空间不是同一块,而是各自释放自己所指向的那块空间,就不会出现重复释放的问题。
初始化列表
就是构造函数初始化成员属性的方式。我觉得跟传统方式差别不大,看个人需要选择初始化方式。
class Person
{
public:
Person(int a , int b , int c) : m_a(a) , m_b(b) , m_c(c)//初始化列表
{
}
int m_a;
int m_b;
int m_c;
}
静态成员
在成员属性和成员函数前加上 static 关键字,就可以将成员属性和函数变为静态的。分为静态成员属性和静态成员函数。
静态成员属性:1、所有对象共享同一份数据。2、在编译阶段就分配好内存。3、必须要类内声明,类外初始化。如果不初始化一个值,后面没法去用它。
可以通过两种方式去访问静态成员属性:1、通过对象去访问。2、通过类名去访问。静态成员属性是有访问权限的。
静态成员函数:1、所有对象都共享同一个函数。2、静态成员函数只能访问静态成员属性。
可以通过两种方式去访问静态成员函数:1、通过对象去访问。2、通过类名去访问。静态成员函数也是有访问权限的。
class Person
{
static int m_A;//静态成员属性--类内声明
static void func()//静态成员函数
{
m_A = 100;//合法操作
//m_B = 100;//错误操作--静态成员函数只能访问静态成员属性
}
static int m_A;
int m_B;
};
int Person::m_A = 10;//静态成员属性--类外初始化--同时加上作用域,不然会被认为是一个全局变量
int main()
{
Person p;
cout << p.m_A << endl;//通过对象去访问静态成员属性
cout << Person::m_A << endl;//通过类名去访问静态成员属性--就是利用作用域
p.func();//通过对象去访问静态成员函数
Person::func();//通过类名去访问静态成员函数--就是利用作用域
return 0;
}
注意:为什么静态成员属性和静态成员函数可以直接通过类名去访问?
因为静态成员是属于类的而不是属于对象的。每个对象都共享同一份静态成员属性和函数,不用创建对象也可以访问到。
为什么静态成员函数只能访问静态成员属性?
因为静态成员属性是所有对象共享同一份数据,属性属于类,不会产生歧义。而普通成员属性属于对象,每一个对象都有一个自己的这个属性,但是静态成员函数是所有对象共享,如果通过静态成员函数去访问普通成员属性,系统不知道你要访问的是哪一个对象的该属性。
成员属性和函数的存储
成员属性和成员函数是分开存放的。非静态成员属性是属于对象的,而静态成员属性和成员函数(不管是不是静态的)是属于类的,他们本质上都是在共享同一份数据。
注意:为什么静态成员函数和非静态成员函数都是属于类的?那他们是怎么区分不同的对象的?
成员函数中有一个概念叫做 this 指针,成员函数就是通过这个指针来区分不同对象调用成员函数时,是谁在调用该成员函数的。
this 指针
this 指针是C++ 中的一个概念。存在于类的成员函数中,用来指向调用函数的对象。即谁在调用这个函数,这个函数的 this 指针就指向哪个对象。隐含在函数中,不用另外声明,可直接使用。
1、为了解决名称冲突问题 -- 如果成员函数的形参名称与类内的成员属性的名称相同,就会导致无法正确为成员属性赋值,加上 this 指针后就可区分出哪个是成员属性,哪个是函数形参。
2、返回对象本身使用 *this -- 这里有一个链式编程思想,即通过 *this 来返回对象本身,就可以继续在后面追加代码,实现链式编程。
class Person
{
public:
void fun(int b,int c)
{
this->b= b;
this->c= c;
}
Person& addNum(Person& p)//返回类型为 对象的数据类型的引用,参数为引用传参
{
this->m_B += p.m_B;
return *this;//*this 返回对象本身
}
int b;
int c;
};
int main()
{
Person p1;
Person p2;
p1.fun(10,20);
p2.fun(20,10);//不同对象在调用同一个函数时,this 指针指向不同的对象
p1.addNum(p2).addNum(p2);//通过返回的对象本身,即可直接追加代码,实现链式编程
return 0;
}
const 修饰的成员函数和对象
当一个成员函数用 const 修饰后,就变成了常函数。而常函数是不允许修改成员属性的值的。一个对象在实例化时在前加上 const 修饰,这个对象就变成了常对象。而常对象也不允许修改成员属性的值。如果一定需要通过常函数和常对象来访问成员属性,可以在成员属性前加上 mutable 关键字。此时标明这是一个特殊变量,可以通过常函数和常对象来访问并且修改属性的值。
class Person
{
public:
void test() const//常函数,不能修改成员属性的值
{
//m_A = 10;//错误的,此时属性的值不可修改
m_B = 10;//正确的,该属性使用关键字 mutable 修饰,成为一个特殊变量
}
int m_A;
mutable int m_B;
};
int main()
{
const Person p;//常对象
//p.m_A = 10;//错误的,常对象不能访问成员属性
p.m_B = 10;//正确的,该属性使用 mutable 修饰,成为一个特殊变量
return 0;
}
注意:为什么加上 const 修饰后常函数不能修改成员属性的值?
因为成员函数中隐含有 this 指针,原本的成员函数格式为Person * const this ...加上const 修饰后,变为 const Person * const this ...,原本是有一个 const 修饰 this 指针的指向,因此指针的指向不可改,另外用一个 const 来修饰该函数时,会在前面在加上一个 const 来修饰 this 指向的值,因此不能通过常函数来修改成员属性的值。常对象同理。
void test()//this 指针的格式为 Person * const this -- 修饰的是指针的指向
{
}
void test() const //this 指针的格式为 const Person * const this -- 修饰指针的指向和指向的值
{
}
友元
利用 friend 关键字,声明一个函数或者一个类为另外一个类的友元。可以使友元在类外访问到该类的私有成员。--理解为好朋友的意思
1、全局函数做友元
class Person
{
friend void test();//声明该全局函数为这个类的友元
};
void test()
{
}
2、类做友元
class Person
{
friend class Dog;//声明该类为这个类的友元
};
class Dog
{
}
3、成员函数做友元
class Person
{
friend class Dog;//声明该类为这个类的友元
friend void Dog::Goujiao();//声明 Dog 类的成员函数 Goujiao() 为这个类的友元
};
class Dog
{
public:
void Goujiao();//类内声明
}
Dog::void Goujiao()//类外实现
{
}
运算符重载
就是将系统已有的运算符重新定义,使它能更加灵活的实现我们想要的功能。利用 operator 关键字。一般使用全局函数去实现重载,使用成员函数有时候会出点小问题。
返回类型 operator 运算符 (参数列表) { }
//全局函数实现重载
Person operator + (Person& p1,Person& p2){ }
因为系统定义的运算符只能进行系统已有的数据类型之间的运算,例如整型相加...但是如果有程序员自定义的类或者数据类型,这些数据类型之间的运算系统就无法满足,这时就需要进行运算符重载,重新定义运算符的功能让它能够实现系统已有的数据类型以外类型的计算。
1、+ 运算符重载
class Person
{
public:
Person(int a, int b)
{
this->m_A = a;
this->m_B = b;
}
//成员函数实现 + 重载
Person operator + (Person &p)
{
Person temp(0,0);//创建临时对象
temp.m_A=this->m_A + p.m_A;
temp.m_B=this->m_B + p.m_B;
return temp;//返回临时对象的值
}
int m_A;
int m_B;
};
//全局函数实现 + 重载
Person operator + (Person& p1,Person& p2)
{
Person temp(0, 0);
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
int main()
{
Person p1(10,20);
Person p2(20,10);
Person p3 = p1 + p2;// + 运算符重载使用--可直接简化为 一个数 + 一个数
cout << p3.m_A << " " << p3.m_B << endl;
return 0;
}
除此之外,还有一些其他的运算符,左移运算符重载(<<)、递增运算符重载(++)、赋值运算符重载(=)、关系运算符重载(==、!=、<、>)、函数调用运算符重载(这个函数调用运算符其实就是小括号 ())。
面向对象的三大特性
封装
封装定义
封装就是给类内成员增加访问权限来实现不同的功能,对成员进行更好的管理。
class 和 struct 都是封装,他们的区别就是默认权限的不同,class 的默认权限为私有,struct 的默认权限为公共。
访问权限
封装特性
将成员变量的访问权限设为 private ,然后提供一个成员方法接口来间接访问私有成员变量,类外成员可以调用这个接口来访问这个私有成员变量。这就是封装的特性,为了达到隐藏成员变量的目的。这就是成员属性私有化。
好处就是可以限制访问成员属性的读写权限,也可以检测数据是否合法(数据有效性)。
封装语法
class Base
{
private:
int num;
public:
void setNum(num)//提供接口供类外为类内私有属性赋值
{
this.num=num;
}
int getNum()//提供接口供类外访问类内私有属性
{
return this.num;
}
};
继承
子类可以继承父类的所有公有属性和保护属性,但不能继承到父类私有属性(不管什么继承方式)。继承主要是为了提高代码复用率,减少重复代码的编写。
继承方式
有三种:public 、private 、protected
public:公有继承,将父类中公有和保护权限的属性和方法都可继承过来,在父类中是什么权限,继承过来后在子类中也是什么权限。
private:私有继承,将父类中公有和保护权限的属性和方法都继承过来,并且都变为子类中的私有权限。
protected:保护继承,将父类中公有和保护权限的属性和方法都继承过来,并且在子类中都变为保护权限。
继承语法
class Base
{ };
class Son : public Base//公有继承
{ };
class Son : private Base//私有继承
{ };
class Son : protected Base//保护继承
{ };
同名成员
如果父类和子类中出现了同名成员,要访问子类的该成员的话,直接通过子类对象去访问就可以。如果要访问到父类中的该成员,就需要加上父类作用域。
如果子类和父类中出现了同名成员函数,要访问子类中的该函数时可以直接访问,如果要访问父类中的该函数,就需要加上作用域,因为如果子类中出现一个和父类成员函数同名的成员函数,系统会将父类中的同名函数全部隐藏,只有加上作用域后才能访问到。
同名静态成员
同名静态成员的访问方法与同名成员一致。加上作用域即可。
多继承方式
一个子类可以继承多个父类。这样做很有可能引发同名成员的存在,需要添加作用域来区分。
C++实际开发中不建议使用多继承方式,根据自身需求。
class Son: public Base1 , public Base2 , ...
{
};
菱形继承
(又叫钻石继承):有两个派生类继承同一个基类,又有一个派生类同时继承这两个派生类。这里的基类和派生类都是相对而言的。
多态
对于同一个行为,不同的对象去完成这个行为会产生不同的结果。
分为静态多态和动态多态。
静态多态
在编译阶段就确定函数的地址,(函数名复用),属于地址早绑定,函数重载和运算符重载就是静态多态。
动态多态
在运行阶段再确定函数的地址,属于地址晚绑定,派生类和虚函数的运用就是动态多态。
动态多态的满足条件
1、类之间有继承关系。
2、子类函数重写父类虚函数。
在父类中将一个函数写为虚函数,在派生的子类中再重写一遍这个虚函数,就可以使得不同的子类去调用这个函数时产生不同的结果,这时的这个函数就是由子类来具体定义。
class Base
{
virtual void test()//父类虚函数
{
cout<<"这是一个基类虚函数!"<<endl;
}
};
class son : public Base
{
virtual void test()//子类重写父类虚函数--这里的 virtual 可写可不写
{
cout<<"这是一个子类虚函数!"<<endl;
}
};
动态多态的使用条件
父类指针或者引用指向子类对象。
Base *base =new Son;
Base &base =new Son;
纯虚函数
就是在虚函数的基础上,直接将这个虚函数置为0,即没有函数体;注意:当一个类拥有纯虚函数时,这个类就变成了抽象类,抽象类的特点是无法实例化对象,只能被继承,然后通过派生类来实例化对象。在派生类中必须重写这个纯虚函数,否则这个派生类也属于抽象类。
virtual void test()=0;
虚析构和纯虚析构
一般来说,变量和对象都是由系统开辟在栈区,如果程序员使用 new 在堆区开辟内存,需要程序员手动释放内存空间,就需要使用析构函数,但是在多态的条件下,一般的父类析构函数无法执行到子类的析构函数,就会导致内存空间释放不完全,造成空间浪费。此时就需要使用到虚析构。
class Base
{
~Base(){}//析构函数
virtual ~Base(){}//虚析构
};
虚析构与虚函数一样,可以让子类重写析构函数,通过这个方法使得在多态条件下,父类指针或者引用可以释放子类对象。
纯虚析构就是在虚析构的基础上将其置为0。有了纯虚析构函数之后,该类也属于抽象类,无法实例化对象。
virtual ~Base()=0;
注意:虚析构和纯虚析构与虚函数和纯虚函数不一样,虚析构和纯虚析构需要在类内声明,需要在类外实现,而虚函数和纯虚函数则只需要声明即可。虚析构和纯虚析构只能存在一个。因为父类可能也需要释放父类对象,需要执行父类的析构函数。
class Base
{
virtual ~Base();//虚析构
virtual ~Base()=0;//纯虚析构
};
~Base()
{
}
文件操作
对文件操作的原因就是 每次程序运行所有的数据都是临时的,在全部程序语句执行完之后就会被全部释放掉,这就导致了程序的数据没办法进行长时间的保存,此时就需要用到文件操作,将程序运行的数据都存到一个文件中,这样就相当于保存在了本地,不会在程序执行完时被释放。
文件的种类
分为文本文件和二进制文件。
文本文件:就是普通的以文本形式存储的文件,一般可以直接读懂。
二进制文件:以二进制的形式存放在文件中,一般不能直接读懂,但是只要在重新加载在程序中时,内容没有发生变化和错误,这个文件保存就没有出错。
文件操作三大类
读操作:ifstream
写操作:ofstream
读写操作:fstream
这里的操作方式都是一个类,需要通过类来创建对象才能使用。我觉得直接使用 fstream 好了,省的在使用时重新创建流对象来实现读跟写,这个直接两个一起实现,更方便!!!
打开方式
步骤流程
文本文件
#include<iostream>
using namespace std;
#include<fstream>//包含头文件
int main()
{
ofstream ofs;//输入方式创建流对象--写文件
ofs.open("test.txt",ios::out);//打开文件,参数为文件路径,打开方式
ofs << "666" << endl;//往文件里写东西--利用 << 运算符
ofs << "777" << endl;
ofs.close();//关闭文件
ifstream ifs;//输出方式创建流对象--读文件
ifs.open("test.txt",ios::in);//打开文件--参数为文件路径和打开方式
if (!ifs.is_open()) //判断是否打开成功
{
cout << "文件打开失败!!!" << endl;
}
//读数据
//第一种方式--字符数组,将文件中的字符读到数组中,在打印
char buf[1024] = { 0 };//创建字符数组
while (ifs>>buf)//利用 >> 运算符--读到文件尾会自动返回一个 fasle 让循环结束
{
cout << buf << endl;
}
//第二种方式--利用 ifs.getline() 函数将字符读到数组中,参数为:数组名,数组大小
char buf[1024] = { 0 };
while (ifs.getline(buf, sizeof(buf)))
{
cout << buf << endl;
}
//第三种方式--利用 getline() 函数将字符读到字符串中,参数为:流对象,字符串名
string buf;
while (getline(ifs, buf))
{
cout << buf << endl;
}
//第四种方式--利用 get() 函数一个一个字符的读出来,并判断是否到文件尾部
char c;
if ((c = ifs.get()) != EOF)//EOF -- end of file 文件尾部的意思,会读到一个结束字符表示文件结束
{
cout << c;
ifs.close();//关闭文件
return 0;
}
注意:读文件的方式有很多种,根据需要选择。第四种方式很少使用。打开方式可以叠加使用,使用 | 符号来追加使用。
二进制文件
二进制文件非常强大,不仅可以操作系统本来有的数据类型数据,也可以操作程序员自定义的数据类型数据,将它们存到文件中。因为时二进制形式存到文件中,所以直接打开可能会出现乱码,不过不要紧,只要在程序中利用读方式读出来后没有出错,就说明文件内容没有出错。
#include<iostream>
using namespace std;
#include<string>
#include<fstream>//包含头文件
class Person//创建好一个测试类
{
public:
char name[64];//这里使用字符数组比 string 更好
int num=0;
};
int main()
{
ofstream ofs;//创建流对象
ofs.open("text.txt", ios::out | ios::binary);//打开文件--并使用 | 符号叠加使用打开方式
Person p1 = { "张三",18 };//创建一个对象--隐式转换法赋值
ofs.write((const char*)&p1, sizeof(p1));//写文件--利用 write() 函数--参数为:传入要写入文件的数据的地址并且强转为 const char*,写入文件的大小
ofs.close();//关闭文件
ifstream ifs;//创建流对象
ifs.open("text.txt", ios::in | ios::binary);//打开文件
if (!ifs.is_open())
{
cout << "文件打开失败!!!" << endl;
}
Person p2;//创建一个对象来存放读出来的数据--因为之前写入的数据就是这个类型的对象
ifs.read((char*)&p2,sizeof(Person));//利用 read() 函数来读出数据,参数为:存放数据的地址,存放数据的空间的大小
cout << p2.name << " " << p2.num << endl;//测试
ifs.close();//关闭文件
return 0;
}
注意:这里用自定义类型来测试,实测发现,如果类中的属性 name 用 string 的话,在后面读文件时会出现访问冲突,改成字符数组就不会出现了。我猜测可能是底层设计的问题,不过我也是看了别人解释才知道的,如果有大佬看到这里,能否给小弟仔细解释一番,实在没明白原因。
尾声
到这里,C++的学习就暂告一段落了,实在是太难了,很多东西都只是明白其表面意思而无法得知其深层含义。但也只能慢慢来了,我的专业课老师告诉我说:学习没办法急于求成,知识的积累是一个漫长的过程。写这篇文章有主要也是方便自己再回头看自己学过的东西,更好回忆也更好找,能学到有一点算一点辣,慢慢来慢慢来!!!
最后附上 github 的链接,有一些自己练习的特别特别简单的代码
https://github.com/little-nan-feng/c-code/tree/54029678506550c267a10d531f29a561e6430312