C++初学者学习笔记(持续更新中)

简介

​ C是面向过程,C++面向对象

​ C语言中的结构=C++中的类

​ C中所谓的结构:学生:学号:名字:性别:成绩

​ 在C语言中,要用结构的话,要先定义一个属于该结构的变量

​ 这个变量在C++中叫做对象

​ 在类中除了可以定义成员变量,还可以定义一些属于该类的函数或者方法,实现一些功能,代码可以这样写:

struct dagongren
{
	//这里有一些方法
	void qichaung();
	void shuaya();
	void shangban();
	//提供一些对外的接口,以应付应急事件
	void tufashijian(int eventtype);
    int money
};

struct dagongren zhangsanfeng;//zhangsanfeng是一个对象,定义成打工人这个类就能调用打工人这个类的方法,否则调用不了
zhangsanfeng.qichaung();
zhangsanfeng.chuanyifu();
zhangsanfeng.money=100;

​ 以上这种设计方式,把功能包在类中,需要的时候通过定义一个对象的方式来调用程序,把这种程序的设计方式叫做基于对象的程序设计方式。这与面向过程的程序设计方式不同

​ 新打工人职业tuixiaoyuan(打工人的方法都有,而且还有额外唱国歌的接口,推销员这个类可以继承自打工人这个类的各种方法,同时又可以增加自己的新方法,在C++中叫做继承性)

​ 多态性:当父类函数和子类函数同时定义了一个同名函数或方法时,应该执行父类的方法还是子类的方法?这时已经不叫基于对象设计,这时叫做:面向对象程序设计

​ 基于对象设计和面向对象设计的区别之处在于在基于对象设计的基础上增加了继承性和多态性是的基于对象程序设计变成面向对象程序设计

​ 这种面型对象程序设计的优点是什么?1.好维护:继承自某一类,并且拥有特有的函数或者方法,所以维护和修改都在自己定义的类中实现,每个类只管自己的接口 2.易扩展:只要继承某一个类,就能使用那个类的方法,所以很容易扩展 3.模块化:通过设置各种访问级别,来限制哪些方法不能被外界访问,哪些方法子类也无法继承,这样也保护了数据安全性。

编写一个最简单的c++程序

int main()
{
	return 0;//0代表成功,其他的数代表失败	
}

​ main函数是整个程序的入口函数,系统就是从该函数开始执行的

工程文件构成

1.在一个工程中,可能包含多个.cpp和多个.h文件,.cpp文件一般叫做源文件,.h文件一般叫做头文件,一些公共的定义都放在头文件里,比如一些类啊,函数声明啊,类啊,define啊一般都存放在h文件

2.不同的编译器有不同的源文件后缀,例如: .c .cpp .cc .m .mm

头文件一般是.h文件,但是有些时候会把定义和实现都写在同一个文件里: .hpp

#include里面又调用了#include<stdio.h>

3.可移植性问题:

​ C++是编译型语言,在执行之前需要把文件编译成可执行的二进制文件,另外还有一种语言叫做解释性语言,先解释再运行,显然编译型语言执行速度更快

​ 比如Lua就是解释性语言,一个工程可以生成一个可执行文件,C++会把每一个源文件都编译生成目标文件,编译系统通过链接,把多个目标文件连接在一起,最后形成一个可执行文件,但是这个可执行文件不能在windows和Linux上同时运行,而源代码拿到不同的平台上都是可以运行的,只是生成的可执行文件不同,但是最后都能运行在不同的机器上。

局部变量及初始化

​ 局部变量:随时用到随时定义

#include<iostream>

using namespace std;

for(int i=0;i<10;i++){//i的生命周期就在这个for循环里,作用域仅仅限制在for语句内

	cout<<i<<endl;

}

​ int abc=2;//这不叫复制,是在定义的时候初始化

​ 或者int abc{2};或int abc={2};

​ 数组定义的时候初始化

#include<iostream>

using namespace std;

int main()
{
	int a[]{1,2,3,4,5};
	for(int i{0};i<5;i++)
	{
		cout<<a[i]<<" ";
	}
}

auto:变量的自动推断

​ auto可以声明变量的时候根据变量的初始值的类型自动为此变量选择匹配的类型,发生在编译期间,所以使用auto不会使得程序执行效率降低,定义一个变量的时候,我们可以这样做:

auto b=true//自动识别为bool类型,bool类型只包含true或者false
auto a='a'//自动识别为char类型
auto c=3//自动识别为int类型

头文件防卫式声明:

​ 在工程中,我们常常遇到头文件之间互相声明的情况,当主函数再次声明了这些头文件时,可能就会出现不同函数或变量重复定义的问题,那么怎么解决这种问题呢?我们知道,在C语言中,ifdef,ifndef是条件编译,我们可以利用这个来防止头文件被重复声明而带来的一系列冲突,方法如下:

#ifndef  标识符//当标识符,没有被define定义过,则对程序段进行编译,
	//程序段
#endif

​ 头文件通用的改造方法:

#ifndef __Head1__//标识符一定保证具有唯一性
#define __Head1__
...
#endif

引用&(为变量起了另外一个名字)

​ 起完别名后,这个别名和变量本身我们就可以看成是同一个变量

int value=10;
int &newname=value//这里不是求地址运算符,只起一个标识符的作用

​ 定义引用并不占用额外内存,或者理解为,引用和原变量占用同一块内存,而且定义引用的时候必须初始化,否则找不到给谁初始化,可以绑定到变量,可以绑定到对象,但是不能绑定到常量,引用的类型也要相同

int a=3;
int &b=a;//这里是引用,&符号在=左边
int *p=&a;//这里是取地址,&在=右边
#include<iostream>

using namespace std;
void func(int a,int &b)
{
	a=0,b=1;
}
int main()
{
	int a=10,b=12;
	cout<<a<<" "<<b<<endl;
	func(a,b);
	cout<<a<<" "<<b<<endl;
	return 0;	
}

注意的是:

(1)引用必须初始化

(2)引用不能单独存在,也不能改变指向

(3)普通引用不能用常量或临时值初始化

(4)引用与指针的区别:1.引用必须在创建时被初始化。指针可以在任何时间被赋值。2.不存在空引用,引用必须连接到一块合法内存。指针可以是空指针3.一旦引用被初始化为一个对象,就不能被指向另一个对象。指针可以在任何时候指向到另一个对对象。

int a,b;
int &T1;//错误,引用必须初始化
int &T2 = a;//正确,a此时是一个变量,常引用能够用变量来初始化
T2 = b //错误,T2此时是一个引用,不能改变指向
int a = 10;
const int b = 20;
int & T1 =  a;//正确,此时a仍然是个变量,普通引用能够用变量来初始化
const int &T2 = a;//正确,常引用可以用变量来初始化
const int &T3 = a+2;//正确,常引用可以用临时值初始化
int &T1 = a+2//错误,普通引用不能被临时值初始化

常量(不变的量)

​ 在C++中const表示不变

const int var=7;//一种承诺,这个变量的值我不会去改变(命名常量)

​ 其实还是有办法改的,那就是引用,

int &var2=(int &)var;//强制转换成引用

命名空间简介、基本输入输出精解

​ 命名空间就是为了防止名字冲突而引入的,系统中可以定义多个命名空间,可以把命名空间看成作用域,在一个命名空间里定义的函数在其他的命名空间里即使同名,也互不影响,命名空间的定义方法:

namespace 命名空间名
{
	...
}//这里不加;

​ 使用场景:假设一个项目,给张三分配了10个要编写的cpp文件,给李四分配了10个要编写的cpp文件,但是巧合的是张三和李四最后写出来的cpp文件中有相同的函数名,而且函数的输入输出都一样,这时如果不使用命名空间就会产生同名函数的错误,编译器会停留在编译阶段,然后抛出错误,而解决这种问题,就要使用我们刚学到的命名空间,下面给出用法:

​ 张三写的源文件:

#include<cstdio>//这里也可以写成#include"stdio.h"
namespace zhangsan//定义一个命名空间
{
	void func()
	{
		printf("张三的cpp文件");
	}
}

​ 李四写的源文件:

#include<cstdio>
namespace lisi//定义一个命名空间
{
    void func()
    {	
        printf("李四的cpp文件");
	}
}
//张三甚至可以在李四的命名空间里定义张三想写的函数,如果首次使用张三的命名空间,就会创建,否则就会追加
namespace zhangsan//张三想在李四的源文件里写自己的函数
{
	void func_in_lisi()
	{
		printf("张三在李四的cpp文件中定义的函数");
	}
}

​ 一个工程只能有一个主函数main(),这也是工程的起点,那么问题来了,外界怎么访问不同命名空间中定义的同名函数呢?这里引入作用域运算符 ::

#include<iostream>
void main()
{
	zhangsan::func();//张三的命名空间中定义的func函数
	zhangsan::func_in_lisi();张三在李四的命名空间中定义的函数
	lisi::func();
	return 0;
}//你觉得这个函数能编译过吗???

​ 答案是否定的,这个程序编译不了,为什么呢?因为main函数所在的源文件都不认识张三和李四命名空间里的那些函数,这就需要开发者养成良好的源代码组织能力,遇到这种类似的情况,可以另外再创建一个头文件,这个头文件里声明了张三和李四定义的所有函数,假设我们新建了一个head1的头文件:

#ifndef __HEAD1__//这种写法是为了防止什么情况呢?如果忘了,去上面找找
#define __HEAD1__
namespace zhangsan
{
    void func();
    void func_in_lisi();
}
namespace lisi
{
    void func();
}
#endif

​ 这样我们就可以通过主函数所在的文件中声明head1头文件,从而让编译器识别张三和李四的命名空间中的函数:

#include"head1"
#include<iostream>
void main()
{
	zhangsan::func();//张三的命名空间中定义的func函数
	zhangsan::func_in_lisi();张三在李四的命名空间中定义的函数
	lisi::func();
	return 0;
}

​ 虽然解决了同名函数的冲突,但是如果每次调用函数都要用一次作用域运算符,这样太麻烦了,有没有更为简洁的办法呢?有

using namespace lisi//在一个源文件中using原则上只能指向一个命名空间,但是如果一定要同时使用两个命名空间,那就一定要保证两个命名空间中不能包含同名函数,否则编译器报错

基本输入输出

基本输出

​ C++中我们不用printf,而是使用C++提供的标准库,我们要把学习C++标准库作为学习C++语言的重要组成部分

#include<iostream>//标准输入输出流;流就是: 一个字符序列,这是一个头文件,没有拓展名
std::cout<<"Welcome to learn C++";//std(标准库命名空间)是系统定义的命名空间,和之前定义的张三,李四的命名空间类似
//如果你不想每次cout时都加std,你也可以加:  using namespace std;

​ 上述代码中的cout在C++中是一个对象,"标准输出"对象

​ <<: 这里其实对<<这个左移运算符做了重载,表示"将:右边的值写到cout这个对象里面去"

​ 举个例子,熟悉一下cout的简单用法:

int x=3;
std::cout<< "x的值是:" << x << "\n";//在C中"\n"是回车,但是在C++中,我们一般不用它,而是std::endl;
std::cout<< "x的平方是:" << x*x << std::endl;//这里的std::endl意思就是,在std标准库命名空间中有个endl的对象,可以实现换行功能,而且endl还有一个额外功能,强制刷新输出缓冲区,也就是说,当你使用endl后,输出缓冲区的数据都会被系统清除。那什么是输出缓冲区呢?输出缓冲区我们可以理解为它是一块内存,cout输出数据实际上就是往输出缓冲区写入数据,操作系统定期从输出缓冲区取出数据,然后送到外部设备接口,操作系统刷新缓冲区(从输出缓冲区取数据)大致分4种情况:(1)缓冲区满了(2)程序执行到main的return语句(3)调用了std::endl(4)当系统不太繁忙时,系统也会查看缓冲区内容,发现新内容也会正常输出到屏幕

​ 如果看完上面的介绍还是感觉懵,那就只记:endl的作用就是把输出缓冲区的内容硬往屏幕上写

基本输入

​ C++中基本输入是cin,举个例子,熟悉一下cin的简单用法:

#include<iostream>
using namespace std;
int main()
{
    int number1=0;
    int number2=3;
    cout<<"请输入两个数:"<<endl;
    cin>> number1 >> number2;
    cout<< number1 <<"和"<< number2 <<"相加结果为:"<< number1+number2 <<endl;
    return 0;
}

​ cin代表键盘,>>右侧代表变量,表明把来自键盘输入的数据送入cin这个对象,cin是一个iostream相关对象,叫"标准输入",可以直观理解为键盘。

​ 写到这里,不知道是否产生这样的疑问,为什么cout和cin可以连续使用<<或>> 呢?

​ 其实对于cin或cout来说,每次都是给cin或者cout对象传入数据,当连续使用<<或>>时,可以等价为这样的写法:

cin>> number1 >> number2;
((cin>> number1)>>number2);//具体讲解一下这段代码,cin>>number1返回的是cin的对象,这个对象可以接着当作左值继续接受数据,并且继续返回cin对象。cout原理一样,这个不理解也没关系,只要会用就行

范围for语句

​ 范围for语句可用于遍历一个序列

#include<iostream>
using namespace std;
int main()
{
    int v[]{12,13,14,15,16};
    for(auto x:v)//将v数组中的元素依次拷贝到x中
    {
        cout<<x<<" ";
    }
    return 0;
}

​ 或者换一种写法

#include<iostream>
using namespace std;
int main()
{
    for(auto x:{12,13,14,15,16})//将v数组中的元素依次拷贝到x中
    {
        cout<<x<<" ";
    }
    return 0;
}

​ 从编程语言的角度来看,上面两端代码没有任何问题,但是从程序性能的角度来看,每次都要拷贝,这会产生很多不必要的时间开销,有没有优化的办法呢?

​ 我们可以尝试使用&

#include<iostream>
using namespace std;
int main()
{
    for(auto &x:{12,13,14,15,16})//这里使用引用就不会每次循环都去拷贝一次,起别名的方式从内存角度来看,没有开辟额外的内存空间(但是实质上是额外开辟了,只是编译器做了一点手脚导致内存地址看起来一样,不过我们可以理解为它们使用了同一个内存单元)。
    {
        cout<< x <<" ";
    }
    return 0;
}

动态分配内存问题

​ C语言将内存分为(1)供程序使用的存储空间(2)静态存储区(3)动态存储区

​ C++中将之进一步细分为5个区

​ (1)栈:一般函数内的局部变量都会放在这里,由编译器自动分配和释放

​ (2)堆:程序员malloc或new分配,用free或delete释放,在编程过程中,malloc申请的内存要及时使用free释放,new申请的内存要及时使用delete释放,否则可能会内存泄漏或者资源耗尽,虽然程序运行结束之后系统会统一回收一次内存,但是有些程序是全年运行的,这就要求在编程的时候要养成良好的习惯,及时释放内存。

​ (3)全局/静态存储区:放全局变量和静态变量static,程序结束时系统释放。

​ (4)常量存储区:例如一些字符”I’m Chinese“

​ (5)程序代码区:存放代码

​ C++中我们重点研究堆和栈,那堆和栈到底有什么区别呢?

​ (1)栈空间有限,分配速度快,程序员控制不了

​ (2)堆:只要不超出实际拥有的物理内存且在操作系统允许能够分配的最大内存大小之内,都可以分配,非常灵活,能够随时new/malloc,free/delete,但是分配速度要比栈慢。需要注意的是,malloc和free在C语言中的定义是函数,所以使用的时候不要忘记加括号。

void *malloc(int NumBytes);//NumBytes要分配的字节数

​ 举个例子:

#include<iostream>
using namespace std;
int main()
{
	int *p = NULL;
	p = (int *)malloc(sizeof(int));//在堆中分配4个字节大小的空间,malloc原始时候是void *,能够被强制转换成任何类型的指针,这里p是int*型,所以强制转换为int*类型指针
	*p = 5;
	cout<< *p << endl;
	free(p);
    return 0;
}
#include<iostream>
using namespace std;
int main()
{
	char *p = NULL;
	p = (char *)malloc(10*sizeof(char));//在堆中分配4个字节大小的空间,malloc原始时候是void *,能够被强制转换成任何类型的指针,这里p是int*型,所以强制转换为int*类型指针
	strcpy(p,"hello world!");//strcpy是C中的用法,当p的长度不足以copy目标字符串时就会截断,多余的部分会强行覆盖,这回导致内存不稳定,在C++中一般使用srtcpy_s(),当遇到长度不够时,这个函数会抛出错误,并不会强行覆盖内存
    strcpy_s(p,10,"hello world!");//这里的10相当于告诉操作系统:我能copy的最大长度是10字节,千万不要超过10
	cout<< *p << endl;
	free(p);
    return 0;
}

​ 下面讲解一下C++中运算符的结合性:

​ 什么是右结合呢?a=b=c
​ 先计算等号右侧的,等价于:a=(b=c)

*++p;        *(解引用),前自增 考虑结合性,右结合,相当于*(++p)
++*p;        前自增,*(解引用) 考虑结合性,右结合,所以相当于++(*p);
*p++;        *(解引用),后自增 不考虑结合性 相当于(*p)++

​ 再看看这段代码:

#include<iostream>
using namespace std;
int main()
{
	int *p=(int *)malloc(100*sizeof(int));
	if(p!=NULL)
	{
		int *q=p;
		*q++=1;//*(解引用),后自增 不考虑结合性 相当于(*q)++
		*q++=5;//同理,第二个元素赋值为5
		cout<< *p <<endl;
    	cout<< *(p+1) <<endl;
    	free(p);//用完之后一定要free
	}
    return 0;
}

​ 但是真正写C++代码时,一般都不用malloc和free,而是使用new和free

​ new和delete在malloc和free的功能基础上又干了更多的事,new的一般使用格式:

​ (1)指针变量名 = new 类型标识符;

​ (2)指针类型名 = new 类型标识符(初始值);

​ (3)指针变量名 = new 类型标识符 [单元个数] ;

示例(1):
int *myint = new int;//开辟一个整数类型的内存空间,首地址赋值给myint
示例(2):
int *myint = new int(18);//开辟一个整数类型的内存空间,首地址赋值给myint,且初始值赋为18
if(myint != NULL)
{	
    int *q = myint;
    cout<< q <<endl;
    delete myint;
}
范例(3):
int *myint = new int[100];//开辟一个整数类型的内存空间,首地址赋值给myint,且初始值赋为18
if(myint != NULL)
{	
    int *q = myint;
    *q++ = 1;//myint[0] = 1
    *q++ = 2;//myint[1] = 2
    //此时q已经指向myint[3]
    delete[] myint;//释放myint数组空间,new时候用了[],所以delete时也必须用[],[]里面不能写数组大小
}

nullptr(C++11中引入,代表空指针)

​ nullptr和NULL的区别在哪里呢?

​ nullptr的类型是std::nullptr_t,而NULL的类型是int,他们是两种类型,但是在if语句中又起到同样的作用,那到底怎么区分他们呢?

​ 一般来说nullptr只能用于指针之间的传递,如果给其他非指针类型变量赋值,就会产生错误,但是NULL却不一样,有时可以用来给指针赋值,又是也可以用来给整型变量赋值,显然,nullptr更加安全,所以在指针初始化时能用nullptr就不要用NULL,从而避免指针和变量之间的混淆。

int *myint = new int(18);//new的第二种用法
if(myint == nullptr)//myint是指针类型,判空最好用nullptr
{
	cout<<"myint is nullptr"<<endl;
}

C中结构体的用法

​ 熟悉一下C语言中结构体的定义及使用:``

#include<iostream>
using namespace std;
struct student{
  //成员变量
    int number;//学号
    char name[100];//姓名
};
int main()
{
    student stu1;//定义结构体变量
    stu1.number = 10086;
    strcpy_s(stu1.name,sizeof(stu1.name),"Zhang san");//上文提到过strcpy和strcpy_s用法的区别
    cout<< stu1.number <<endl;
    cout<< stu1.name <<endl;
    return 0;
}

​ C++中的结构和C的结构有什么区别呢?C++中的结构具备了C中结构的所有功能外,还具备最突出的拓展功能之一就是:C++中的结构不仅仅有成员变量,还可以定义成员函数(方法)。

权限修饰符

​ public、private、protect

结构体

​ 先说public和private

​ public是公共的意思,用这个修饰符修饰的类中的成员函数、成员变量,就可以被外界访问。

​ private是私有的意思,用这个修饰符修饰类中的成员函数、成员变量,只有被内部定义的成员函数才能使用

​ 用代码解释一下:

struct student
{
	int number
	char name[100];
    void func()//结构体下创建的成员函数,在结构体类型中一般默认权限是public
    {
        number++;
        return;
	}
};
struct student
{
private://私有的
	int number
	char name[100];
public://公有的
    void func()//结构体下创建的成员函数,在结构体类型中一般默认权限是public,可以看成一个外部接口
    {
        number++;
        return;
	}
};

类简介

​ 不管C还是C++结构都用struct定义,但是类只在C++中才有,在C中定义一个属于该结构的变量,我们将之称为结构变量,在类中我们定义一个属于该类的变量,那它就叫做对象。本质来说,无论是结构变量还是对象,他们都是一块内存。主要区别有两个:(1)结构默认访问权限都是public,类相反,内部成员变量及成员函数默认权限是private。(2)C++结构体继承默认是public,而C++类默认继承都是private

类组织

​ 通常,我们会把类的定义代码放在一个 .h头文件中,这里要区分一下什么是类的定义,什么是类的实现,举个简单的例子:

class student
{
private:
	int number
	char name[100];
public:
    void func();//这里是定义代码
};
void student::func()//这里是实现代码,类的定义代码和实现代码可以分开放在不同的文件中,需要注意的是,跨文件定义时,需要把相应的头文件包含进去
{
     number++;
     return;
}

内联函数

inline int myfunc(int testv)//这里是函数定义,在函数定义前面加一个inline就变成了内联函数
{
    return 1;
}

​ 内联函数有什么功能呢?inline到底是干什么的?

​ 我们知道,调用函数时编译器需要保护现场,压栈出栈要消耗系统资源,如果写的函数体量很小,但是又去频繁调用时很不划算的,所以有了内联函数,inline会影响编译器,在编译阶段对inline这种函数进行处理,系统尝试将调用这种函数的动作替换为调用函数本体,通过这种方式来提升程序性能。

​ inline只是开发者对编译器的一个建议,编译器可以尝试去做,也可以不做,这取决于编译器的诊断功能,也就是说,觉得权在于编译器,我们控制不了。

​ 传统写函数时,我们把函数声明写在头文件里,函数实现写在源文件里,如果把函数实现写在头文件中,当多个.cpp文件包含同一个头文件时,编译器就会报错,我们最好把函数实现放在一个新的源文件中,将所有函数的声明放在统一的一个头文件中,这样就不会产生冲突了。但是,内联函数恰恰相反,内联函数的定义就要写在头文件中,因为系统对内联函数的处理和普通函数的处理有所不同,当某一个源文件包含的某一个头文件中有内联函数,这个内联函数的定义也会随之包含进去,这样系统就能通过头文件中定义的内联函数本体去替代函数调用。

​ 虽然内联函数可以提高编码效率,但是也会带来一个问题:代码膨胀,如果内联函数过大,每次替换时就会扩大代码量,调用次数越多,代码量越大,所以定义内联函数时,一定不能在里面写过多的语句,否则很容易造成代码膨胀。

​ #define(宏展开)也类似于inline,有兴趣可以查阅

函数杂合用法总结

​ 返回值是void类型的函数被另一个返回值是void类型的函数调用返回:

void func1()
{
}
void func2()
{	
	return func1();
}

​ 返回值是指针的函数被另一个返回值是指针的函数调用:

int *func1()//这个函数看似正确,实则存在巨大隐患,因为temvalue这个变量的生命周期就是这个函数,如果离开这个函数,这段内存就会被系统回收,这样会导致函数向外部传了一个不能使用的地址,程序很容易崩溃
{
    int temvalue=10;
    return &temvalue;
}

​ 要避免这种情况,可以这样写:

int temvalue = 10;
int *func1()
{
    return &temvalue;
}
int main()
{
	int *p = func1();
    *p = 6;
    return 0;
}

​ 再例举一种错误情况

int &func1()
{
    int temvalue = 10;
    return temvalue;
}
int main()
{
    int &k = func1();
}

​ 上面这段函数有什么问题呢?当func1函数退出后,会将已经申请的内存返还给操作系统,temvalue是局部变量,当函数返回后系统认为这个内存地址已被回收,不属于任何用户,但是这个函数却把这个已回收的内存地址传了出去,main函数中接受到函数返回的地址之后如果使用这块内存,就会出现错误,避免这种情况的方式一样,就是把temvalue定义成全局变量

​ 函数的声明对编译没有影响,如果只声明了函数,但是却不调用,编译是不会报错的,但是如果声明了函数,但是没有写函数实现,一旦调用,就会出问题。

​ 普通函数,只能定义一次(定义放在.cpp文件中),声明可以声明多次。一般.cpp文件会#include自己的函数声明文件

String类型

​ string类型简介:C++标准库中的类型,表示一个可变长字符串

char str[100] = "I love you";

​ string本身就是一种类,属于std命名空间,使用时需要包含头文件:

#include<string>

定义和初始化string

string s1;// 默认初始化,s1 = "" 空串,表示里面没有字符
string s2 = "I love you";// 把"I love you"这个字符串拷贝到s2代表的一段内存中,这里的拷贝不包括末尾的\0
string s3 = "I love you";// 与s2效果一样
string s2 = s2; // 把s2的内容拷贝到了s4代表的一段内存

int num = 6;
string s5(num, 'a');// aaaaaa,把s5初始化为num个字符a组成的字符串,不推荐这种做法,系统会创建临时变量

String上的操作

string.isempty() //判空

string s1;
if(s1.isempty()) { // 成立
    cout<<"s1为空"<<endl;
}

string.size() string.length() // 返回字符串长度

string s1;
if(s1.isempty()) { // 成立
    cout<<s1.size()<<endl;
    cout<<s1.length()<<endl;
}

字符串对象的赋值

string s1 = "abc";
string s2 = "def";
s1 = s2;// 字符串的赋值,用s2里面的内容取代原来s1里面的内容

判断两个字符串是否相等/不相等

string s1 = "abc";
string s2 = "def";
if(s1 == s2) {// 判断两个字符串是否相等,这里大小写敏感
	cout<< "s1 = s2";
}else {
	cout<< "not equal";
}
if(s1 != s2) {// 判断两个字符串是否不相等
	cout<< "s1 != s2";
}else {
	cout<< "equal";
}

string.c_str()

​ 返回一个字符串string中的内容指针,返回一个指向正规的c的字符串的指针常量,也就是以\0结尾的,此函数引入是为了与C语言兼容,C语言中没有sring类型,所以可以通过string类对象的c_str()把string对象编程C语言中的字符串样式

string s9 = "abc";
string s10 = "abC";
const char *p = s10.c_str();

char str[100];
srcpy(str, sizeof(str), p);
cout<< str << endl;

用C语言的字符数组初始化string类型

char str[100];
string s11(str);

读写String对象

string s1;
cin>> s1;//从键盘输入,输入内容用空格截断,空格之后无效
cout << s1 <<endl;

字面值和string相加

string s1 = "abc";
string s2 = "defg";
string s3 = s1 + " and " + s2 + 'a';//右值中有s1或s2是string类型,所以两边类型相等且不相邻可以相加

反例

string s3 = "abc" + "def";//系统无法判别等式右边的类型,所以无法相加
string s3 = s1 + "and" + "de";//系统识别到右边有两个字符串挨着相加两个字符串严格意义上不能相加,所以编译时会报错

范围for针对string的使用

string s1 = "I love china";
for(auto c : s1) // auto 变量类型自动推断
{
	cout<< c <<endl;
}
//改变string里面的内容
for(auto &c :s1)
{
    c = toupper(c);// 因为c是一个引用,所以相当于改变s1里面的值,toupper()功能:小写字符变大写,大写字符没变化
}

Vector类型介绍

vector类型简介

​ vector类型来自于标准库,代表集合、动态数组的概念。所以可以把若干对象放在里面,前提是这些对象类型相同。换句话说,vector能把其他对象装进去,也被称为容器,使用时需要包含头文件

#include<vector>
using namespace std;
int main()
{
    vector<int> vvvv;//解释一下这段代码,vvvv是一个对象,<int> 表示vvvv这个对象是一个整型对象  
    return 0;
}

​ 示例

#include<vector>
#include<iostream>
#include<cstring>
using namespace std;
struct Student
{
    string name;
};//注意加;
int main()
{
    vector <Student> Yang; //甚至还可以容器套容器 vector<vector<string>> f,代表f集合里边的每一个元素又都是一个vector<string>集合
    vector <int *> Li;//可以装指针
    vector <int &> L;//这样是错的,vector里不能装引用,因为引用只是一个别名,它不是一个对象,所以不能往vector里放
    return 0;
}

定义和初始化vector对象

​ 空vector及其初始化

vector<string> mystr;//创建一个string类型的空的vector容器,这是一个空容器

mystr.push_back("abcde");//这个函数能够往vector容器里面塞入对象
mystr.push_back("def");
//函数执行完上面两条指令后会在内存中以类似于数组的形式存放,比如:mystr是一个集合类型的对象,mystr[0]存放了第一个存进去的对象"abcde"
//mystr[1]里面是"def"

​ 元素拷贝的初始化方式

vector<string> mystr2(mystr);//把mystr里的元素全部拷贝到mystr2里去
vector<string> mystr3 = mystr;//把mystr里的元素全部拷贝到mystr2里去

​ C++11标准中,用列表初始化方式给值,用{}括起来

vector<string> mystr4 = {"aa","bb","cc"};

​ 创建指定数量元素

vector<int> myint(15,-100);//创建指定数量元素,一般都有(),15代表创建15个元素,-100表示初始值是-100
vector<string>mystring(5,"hello");//创建5个string类型的元素,每个元素的初始值是helllo

​ 多种初始化方式

// ()一般表示元素个数,{}一般表示元素内容,不绝对
vector<int> i1(10);//创建10个int类型的元素
vector<int> i2{10};//表示一个元素,该元素的值是10
vector<string> snor{"hello"};//1个元素,内容是hello
vector<string> s22{ 10 };//系统发现10不是字符串,和容器类型不一样,就会把10当作元素数量,创建了10个元素
vector<string> s23{10, "hello"};//10个元素,每个元素的内容都是“hello”
vector<int> i4(10,1);//10个元素,每个元素的值为1
vector<int> i4{10,1};//元素类型吻合,就会创建两个元素,一个元素的值为10,另一个元素的值为1,等同于初始化列表
//结论:要想正常通过{}初始化,大括号里面的值的类型必须要和vector类型存放元素类型相同
vector<int> i1{"hello"};//系统会报错

vector对象上的操作

​ 经常使用时我们不知道vector里有多少个元素,都是动态增减,所以刚开始都是创建一个空的vector对象

#include<vector>
#include<iostream>
using namespace std;
int main()
{
	vector<int> a;
    // 容器判断是否为空
	if(a.empty()) {                         
		cout<<"ventor a is empty";
	}
	else {
		cout<<"vector a is not empty";
	}
    // 向容器里添加元素
    a.push_back(1);
    for(int i=2; i <=100; i++)
    {
        a.push_back(i);
    }
    // a.size() 返回元素个数
    cout << a.size() <<endl;
    // a.clear() 移除所有元素
    a.clear();
    cout << a.size() <<endl;
    // a[b]返回a中的n个元素,代表位置,从0开始,范围:0-a.size(),如果超出范围,编译器检查不出来,可能会发生不可预测的错误
    a.push_back(1);
    cout << a[0];   
    //赋值
    vector<int> a2(100);
    a2 = a;//a2里面100个元素消失,被a里的1个元素冲刷掉了
    a2 = {11,12,13,14,15};//a2里面的1个元素被{}里的5个元素冲刷掉了
    cout<< a2.size();
    //判断是否相等 == 两个vector相等,要保证两个vector里的元素个数相同,并且每个元素的值相等
    if(a2 == {11,12,13,14,15}) {
        cout<<"两个值相等";
    }
    else {
        cout<<"两个值不相等";
    }
	return 0;
}

范围for使用vector

#include<vector>
#include<iostream>
using namespace std;
int main()
{
    vector<int> vecvalue{1,2,3,4,5};
    for(auto &vecitem :vecvalue)// 这里如果不加引用,每次都会拷贝vecvalue的值到vecitem里去,如果加引用,只是起个别名,不会拷贝
    {//如果不修改vecvalue里的值,也可以不用&,但是如果要修改,就一定要加&
        cout << vecitem<<" ";
        vecitem *= 2;
        cout<< vecitem <<endl;
    }
	return 0;
}

​ 在for或者遍历容器时,千万不要改变容器的容量,因为每次遍历开始时都会确定一个结束点,如果改变容器容量,结束点位置混乱,导致无法预知的错误

迭代器演绎,失效分析及弥补

迭代器简介

​ 迭代器是一种遍历容器内元素的数据类型,这种数据类型有点像指针,可理解为,迭代器用来指向容器中的某个元素,C++11中的string,vector很少用[]来访问元素,而是使用迭代器。通过迭代器,就可以都或修改string或vector所指向的元素值。

容器的迭代器类型

vector<int> a = {100,200.300};
vector<int>::iterator pos,iter;//定义迭代器,把vector<int>::iterator看成是迭代器类型,后面变量名字自己定,<int>表示迭代器里元素类型是int

迭代器begin()/end()操作,反向迭代器rbegin()/rend()操作

​ 每一中容器都会定义begin()和end()成员函数,begin()和end()用来返回迭代类型

//begin()返回一个迭代器类型
iter = a.begin();// 如果容器中有元素,则begin返回迭代器,指向的是容器中第一个元素
iter = a.end();//也是返回一个迭代器类型,如果容器中有元素,指向的是容器中末尾元素的后边,这个后边怎么理解?可以理解为它指向了一个
//不存在的元素

//如果vector是空的,则begin和end返回的迭代器相同

​ 传统通用的迭代器访问容器的方法

vector<int> a = {100.200.300};
for(vector<int>::iterator &pos : a.begin())
{
	if(pos ==a.end()) {
		break;
	}
}
//或者
for(vector<int>::iterator pos = a.begin(); pos != a.end() ; pos++)
{
	cout<< *pos <<" ";
}

​ 反向迭代器,用于从后往前遍历一个容器

//rbegin() 返回一个反向迭代器,指向反向迭代器的第一个元素位置
//rend() 返回一个反向迭代器,指向反向迭代器最后一个元素的下一个位置
for(vector<int>::reverse_iterator rpos = a.rbegin();rpos != a.rend(); rpos++)//vector<int>::reverse_iterator可以看成反向迭代器类型
{
	cout<< *rpos <<endl;
}

迭代器运算符

// *iter: 返回迭代器iter所指向的元素的引用,必须要保证这个迭代器指向的是有效的容器元素,不能指向end(),因为end指向不存在的元素
//如果* a.end()程序就会崩溃
iter++;
++iter;//容器指向下一个元素,指向end不能再++

--iter;
iter--;//指向容器中的上一个元素,指向头不能再--

//如果两个迭代器指向同一个元素,则==,否则!=
#include<vector>
#include<iostream>
using namespace std;
struct Student
{
	int number;
};
int main()
{
	Student stu;
	vector<Student> su;
	stu.number = 100;
	su.push_back(stu);
	vector<Student>::iterator pos;
    pos = su.begin();//指向容器中第一个元素
    cout<< (*pos).number;//这里的(*pos)相当于stu,这里提供了两种引用结构中成员的方式
    cout<< pos -> number;//或者这样写,但是一定要确保迭代器是有效的
	return 0;
}
const_iterator迭代器

​ const表示常量,值不能改变,const iterator表示这个迭代器指向的元素不能改变,但并不代表这个迭代器本身不能改变,使用const可实现只读功能。

vector<int> a = {1,2,3,4,5};//非常量容器也可以用常量迭代器遍历,如果是常量容器,就必须用常量迭代器遍历 const vector<int> a
vector<int>::const_iterator const_pos;
for(pos = a.begin(); pos != a.end(); pos++)
{
    cout<< *iter <<endl;//可以正常执行
    *iter = 10;//执行这一句就会报错
}
cbegin()和cend()操作

​ C++11引入的两个新函数,和begin和end类似,但是返回的都是常量迭代器

vector<int> a = {1,2,3,4,5};
for(auto cpos = a.cbegin() ; cpos != a.cend(); cpos++)
{
	*cpos = 10;// 执行这一句就会报错
	cout<< *cpos <<endl;// 执行这一句不会报错
}

迭代器失效

vector<int> vecvalue{1,2,3,4,5};
for(auto &vecitem : vecvalue)
{
	vecitem.push_back(888);// 因为这一句,程序将混乱,因为改变了容器的容量,可能会使指向容量元素的指针,引用,迭代器失效
	cout<< vecitem <<endl;
}
// 等价程序
for(auto beg = vecvalue.begin() ,end = vecvalue.end(); end != beg ; beg++)// 这里用的是迭代器类型
{
    vecitem.push_back(888);// 因为这一句,程序将混乱,因为改变了容器的容量,可能会使指向容量元素的指针,引用,迭代器失效
	cout<< *beg <<endl;
}
// 但是如果非要改变迭代容量呢?,用break
for(auto beg = vecvalue.begin() ,end = vecvalue.end(); end != beg ; beg++)// 这里用的是迭代器类型
{
	cout<< *beg <<endl;
    if(*beg == 1)
    {
        vecitem.push_back(888);
        break;// 当改变了容器容量之后,立刻break出去
    }
}
灾难程序演示1
vector<int> vecvalue = {1,2,3,4,5};
vector<int>::iterator beg = vecvalue.begin();
auto end = vecvalue.end();// 与上句等价
while(beg != end)
{
    cout << *beg <<endl;
    vecvalue.insert(beg, 80);// 插入新值,第一个参数为插入位置,第二个参数为插入的元素
    // 迭代器执行插入操作后改变了容器的容量,程序出现问题,某一些迭代器会失效,具体哪个失效,取决于不同的容器内部实现原理
    // 现在不明确哪个迭代器失效,最明智的做法就是break后继续遍历,
    break;// 防止迭代器失效方法一
}
#include<vector>
#include<iostream>
using namespace std;
int main()
{
    int flag = 0;
	vector<int> vecvalue;
	for(int i=0;i<20;i++)
	{
		vecvalue.push_back(i);
	}
	vector<int>::iterator beg = vecvalue.begin();
	while(beg != vecvalue.end())//每次更新end,防止失效
	{
    	beg = vecvalue.insert(beg, flag + 80);//接着insert返回的结果
    	flag++;
    	if(flag > 10)
   		{
        	break;
    	}	
    	beg++;
	}
	for(vector<int>::iterator start = vecvalue.begin();start != vecvalue.end();start++)
	{
		cout<< *start<<endl;
	}
	return 0;
}

​ 防止迭代器失效最好的解决方法就是,一旦改变容器容量就立刻break出去

灾难程序演示2

​ 以下代码很容易出错:

vector<int> iv = {100,200,300};
for(auto iter = iv.begin(); iter != iv.end(); iter++)
{
	iv.erase(iter)//删除iter位置上的元素,返回下一个元素位置
}

​ 上面使用迭代器方法不当,换一种正确的写法

vector<int> iv = {100,200,300};
vector<int>::iterator iter = iv.begin();
while(iter != iv.end())//每次更新end,避免了演示1的bug
{
	iter = iv.erase(iter);//iter每次都会更新,元素一个一个删除
}

//另一种简单粗暴的删除方法
while(!iv.empty())
{	
    auto iter = iv.begin();//不为空,返回的begin()是有效的
    iv.erase(iter);
}

范例演示

用迭代器遍历一下string类型数据
string str[] = "I love China";
for(auto iter = str.begin(); iter != str.end() ;iter++)
{
	*iter = toupper(*iter);
}
cout<< str <<endl;
vector容器常用操作于内存释放
// 实践程序
// ServerName = 1区   表示服务器名称
// ServerID = 100000
#include<string>
#include<vector>
#include<iostream>
using namespace std;
struct conf {
  	char itemname[40];
    char itemcontent[100];
};

char *getinfo(vector<conf *> &conflist, const char *pitem)
{	
    for(auto pos = conflist.begin(); pos != conflist.end(); pos++)
    {
        if(_strcmp(*pitem, (*pos)->itemname ==  0)
        {
        	return (*pos)->itemcontent;
        }
    }
}
int main()
{
    conf *pconf1 = new conf;
    strcpy_s(pconf1->itemname, sizeof(pconf1->itemname), "ServerName");
    strcpy_s(pconf1->itemcontent, sizeof(pconf1->itemcontent), "1区");
    
    conf *pconf2 = new conf;
    strcpy_s(pconf2->itemname, sizeof(pconf2->itemname), "ServerID");
    strcpy_s(pconf2->itemcontent, sizeof(pconf2->itemcontent), "100000");
    
    vector<conf *> conflist;
    conflist.push_back(pconf1);
    conflist.push_back(pconf2);
    
    char *p_tmp = getinfo(conflist, "ServerName");//查询
    if(p_tmp != nullptr)
    {
        cout<< p_tmp<<endl;
    }
    //释放内存
    vector<conf*>::iterator pos;
    for(pos = conflist.begin();pos != conflist.end(); ++pos)
    {
        delete (*pos);//没有破坏迭代器,只是把迭代其中指向的数据删了
    }
    conflist.clear();//清空容器
    return 0;
}

类型转换

显示类型转换
int k = 5 % int(3.2);//C语言风格的类型转换
int k = 5 % (int)3.2;//效果一样
//C++风格的4种强制类型转换
//提供更丰富的含义和功能,更好的类型检查机制,方便代码的书写和维护
static_cast
dynamic_cast
reinterpret_cast
const_cast
//这四个都是命名的强制类型转换,每一个强制类型转换都有一个名字,名字各不相同,他们都有一个通用的形式
//强制类型转换名<type>(express);
强制类型转换名时四个强制类型转换名之一
type是转换的目标类型
express是你要转换的值
static_cast
//可用于整型和实型之间的转换
double f =1.2.3;
int i = (int)f;
int i = static_cast<int>(f);

//可用于子类转成父类
class A {};
class B = public A{};
B b;
A a;
A a = static_cast<A>(b);//把子类转成父类,但是父类转成子类就不行

//可用于void * 与其他类型指针之间的转换,void * 无类型指针,意思就是可以指向任意指针(万能指针)
int i = 10;
int *p = &i;
void *q = static_cast<int*>(p);
int *db = static_cast<int*>(q);//从int转会int

//一般不能用于指针之间的转换,比如:int*转double*     float*转double*    等
double f = 100.0f;
double *pf = &f;
int *i = static_cast<double*>(pf);//这样是错的,不能这样转
float *fd = static_cast<float*>(pf);//这样还是错的
dynamic_cast
// 主要引用于运行时的类型检查,主要用于父类型和子类型转换用的,它会让父类型指针指向子类型对象
const_cast
// 去除指针 或引用的const属性。该转换能够将const性质转换掉,编译时就要类型转换
const int ai = 90;
const int *pai = &ai;
int ai2 = const_cast<int>(ai);//将const_int转成int
int *pai2 = const_cast<int*>(pai);
*pai2 = 120//虽然语法上没有错,但是最好不要这么干,因为原本这是是个const类型,实际上有可能写进去,也有可能看起来写进去了,但是实际上没有写进去
cout<< ai <<endl;
cout<< *pai <<endl;
reinterpret_cast
// 编译时就会进行类型转换,翻译为:重新解读,将操作数内容解释为另一种不同的类型,主要用于处理无关类型的转换,也就是两个类型之间没有任何关系
// 常用于将一个整型(地址)转换成指针;一种类型指针转换成另一种类型指针,转换后按照转换后的内容重新解释内存中的内容
// 也可以从一个指针类型转换为一个整型
int i = 10;
int *p = &i;
int *p1 = reinterpret_cast<int *>(&i);
char *pc = reinterpret_cast<char *>(p);
// 被认为是一种危险的类型转换,怎么转,编译器都不会报错,关键是没有意义,安全性比较差
隐式类型转换
int m = 3 + 45.6;

综述

​ 类是我们自己定义的数据类型(新类型)

​ 设计类时要考虑的角度

​ (1)站在设计和实现者的角度来考虑

​ (2)站在使用者的角度来考虑

​ (3)父类,子类;

类基础

​ (1)一个类就是一个用户自己定义的数据类型,我们可以把类想象成一个命名空间,包含一堆的东西(成员函数、成员变量)。

​ (2)一个类的构成:成员变量,成员函数。

​ (3)我们访问类成员时,如果类是对象,我们就使用 对象名.成员名来访问成员。

class student
{
  int number;  
};
int main()
{
    student stu;
    stu.number = 1000;
    
    student *s = &stu;//如果是只想指向对象的指针,就用 指针名->成员名  来访问成员。
    s->number = 100;
    
    cout<< stu.number;
    return 0;
}

​ (4)public成员提供类的接口,暴露给外界。private成员提供各种实现类功能的细节方法,但不暴露给使用者,外界无法使用这些private。

​ (5)struct 是默认为public的class

​ (6)class成员默认是private(私有);class A{…} 、struct A{…} == class A{public: …}

成员函数

//c语言写法
class Time
{
public:
    int Hour;
    int Minute;
    int Second;
};
void initTime(Time &stmpTime, int tempHour, int tempMin, int tmpSec)//类里面的time和initTime没有直接关系
{
    stmpTime.Hour = tempHour;
    stmpTime.Minute = tempMin;
    stmpTime.Second = tmpSec;
}
int main()
{
    Time tim;
	initTime(tim, 14, 21, 30);    
    return 0;
}

​ 创建time.h头文件:

#ifndef __MYTIME__
#define __MYTIME__
class Time
{
public:
    void initTime(int tempHour, int tempMin, int tmpSec);//此时叫成员函数
private:
    int Hour;
    int Minute;
    int Second;
};
#endif

​ 创建time.cpp文件

#include"time.h"
void Time::initTime(int tempHour, int tempMin, int tmpSec)// 作用域运算符,相当于把initTime限定在Time这个类中,表示initTime这个函数属于Time类
{
    Hour = tempHour;//成员函数中可以直接使用成员变量名
    // 哪个对象调用了成员函数,这些成员变量就属于哪个对象,可以理解为成员函数知道哪个对象调用了自己
    Minute = tempMin;
    Second = tmpSec;
}
int main()
{
    Time tim;
	tim.initTime(30,21,2);    
    return 0;
}

​ 类是一种特殊的存在,可以在不同源文件种重复定义,与全局变量不同,这种情况是被系统允许的。

对象拷贝
Time myTime;
myTime.Hour = 12;
myTime.Minute = 15;
myTime.Second = 16;

Time myTime1= myTime;// 这里不是赋值,是初始化,定义的时候初始化,这里其实是拷贝的,对象本质就是一段内存
Time myTime2 (myTime);// 用括号也能初始化
Time myTime3 {myTime};
Time myTime4 = {myTime};

私有成员
class Time
{
    private:
        int Millisecond;// 毫秒,私有成员变量,类外无法直接用 .成员变量 调用 但是可以被自己的成员函数调用
    	void initMillTime(int mls);//对象不能调用私有成员函数,意思就是不希望使用者能够调用私有的成员函数,不想被其他用户可知,这样的接口是对内开放的
    public:
        int Hour;
    	int Minute;
    	int Second;
    	void initTime(int tmphour, int tmpmin, int tmpsec);
};
void Time::initMillTime(int mls)
{
    Millisecond = 1000;// 不管成员函数是不是私有,都能直接访问成员变量
    initTime(1,2,3);//成员函数直接也可以互相调用,不管所调用的函数是公有还是私有的
}
构造函数

​ 在类中,有一种特殊的成员函数,它的名字和类名相同,在创建类对象的时候,这个特殊的成员函数就会被系统自动调用,我们叫它”构造函数“,他的功能就是初始化类对象的数据成员。

class Time
{
    private:
        int Millisecond;
    	void initMillTime(int mls);
    public:
        int Hour;
    	int Minute;
    	int Second;
    	void initTime(int tmphour, int tmpmin, int tmpsec);
    // 在这里创建共有的构造函数
    	Time(int tmphour, int tmpmin, int tmpsec);
};
void Time::initMillTime(int mls)
{
    Millisecond = 1000;
    initTime(1,2,3);
}
// 构造函数的实现,特点,构造函数没有返回值,在构造函数前面什么都不写,并且不可以手动调用构造函数,否则编译就会出错
// 正常情况下,构造函数要手动声明为public,因为我们创建一个对象时系统会替代我们调用构造函数,这也说明系统(外界)能够调用构造函数,
// 所以构造函数要设置成public
// 类缺省的成员是私有成员,所以我们必须说明构造函数是一个public函数,所以就无法直接创建该类的对象
// 构造函数中如果有多个参数,创建对象的时候也要带上这些参数
// 如果传参时参数个数与构造函数参数个数不匹配,可以额外再写一个构造函数,系统会根据所传参数的个数去匹配构造函数中符合参数个数的构造函数并匹配
Time::Time(int tmphour, int tmpmin, int tmpsec)
{
    Hour = tmphour;
    Minute = tmpmin;
    Second = tmpsec;
}
int main()
{
    Time myTime = Time(12,13,14);// 创建类对象,创建的时候调用了构造函数
    Time myTime2(12,13,14);
    Time myTime3 = Time{12,13,14);
    Time myTime4{12,13,14};
    Time myTime5 = {12,13,14};
    return 0;
}
隐式转换和explicit

​ 编译系统在私下里干了很多我们所不知道的事情

Time myTime40 = 14;
Time myTime41 = {12 ,13 ,14 ,15 ,16};
构造函数初始化列表

​ 在系统调用构造函数时,可以对类的成员变量进行赋值

class Time
{
    private:
        int Millisecond;
    	void initMillTime(int mls);
    public:
        int Hour;
    	int Minute;
    	int Second;
    	void initTime(int tmphour, int tmpmin, int tmpsec);
    // 在这里创建公有的构造函数
    	Time(int tmphour, int tmpmin, int tmpsec);
};
Time::Time(int tmphour, int tmpmin, int tmpsec)// 推荐使用这种初始化方式
    :Hour(tmphour), Minute(tmpmin), Second(tmpsec)// 不要用同一个参数给多个成员变量赋值,因为赋值的先后顺序取决于类中成员变量的定义顺序
    {
        std::cout<< tmphour << tmpmin << tmpsec;
    }

// 另一种方式,赋值
void Time::initTime(int tmphour, int tmpmin, int tmpsec)
{
    Hour = tmphour;
    Minute = tmpmin;
    Second = tmpsec;
}
在类定义中实现成员函数inline

头文件Time.h

#ifndef
#define __TIME__
class Time
{
public:
	Time(int h, int m, int s);
    void initTime(int h, int m, int s)// 像这种在类内定义的函数,编译时系统会看作内联函数,所以这样的函数一定要简单,inline是个建议,会不会替换还是取决于编译器本身,认为控制不了
    {
        hour = h;
        minute = m;
        second = s;
    }
private:
	int hour;
	int minute;
	int second;
}
#endif

源文件:

#include"Time.h"
#include<iostream>
using nemspace std;
Time::Time(h,m,s)
{
    hour = h;
    minute = m;
    second = s;
}
int main()
{
    Time time;
    return 0;
}
成员函数末尾的const

​ 在成员函数后面添加了一个const,声明和定义都要加,作用:告诉系统,这个成员函数不会修改该对象里任何成员变量的值等,即这个成员函数不会修改Time的任何状态

#ifndef
#define __TIME__
class Time
{
public:
	Time(int h, int m, int s);
    void readdata() const;
private:
	int hour;
	int minute;
	int second;
}
#endif
#include"Time.h"
#include<iostream>
using nemspace std;
Time::Time(h,m,s)
{
    hour = h;
    minute = m;
    second = s;
}
void Time::readdata() const// 不能放在普通函数末尾
{
    cout<< hour << minute << second;
}

int main()
{
    Time time;
    return 0;
}

​ 不管是不是const对象,都可以调用const成员函数

​ const对象调用不了非const成员函数

mutable

​ 不稳定,容易改变的意思,const的反义词,mutable的引入也正好是为了突破const的限制,用mutable修饰定义的成员变量,在const修饰的成员函数中也可以被修改,先当于告诉系统,不管是成员函数还是const成员函数,都可以修改。

返回自身对象的引用,this

​ 头文件:

class Time
{
public:
    Time& add_hour(int tmphour);// 把对象自己给返回去了,实际上 Time& add_hour(Time* const this, int tmphour)
    Time& add_min(int minute);
    Time& add_sec(int second);
}

​ 源文件:

Time& Time::add_hour(int tmphour)
{
    hour +=tmphour;
    return *this// 把对象自己给返回去了,this存放该对象的地址,*this就是该对象的首地址,this是类中隐含的指针,用于指向对象,每次调用成员函数,编译器就会重写,成员函数第一个参数隐含为this指针
        // 在系统看来,任何对类成员的直接访问都被看作是通过this作隐式调用 hour += tmphour   <==>  this->hour += tmphour;
        // this是个常量指针,总是指向对象本身
        // this指针只能在成员函数中使用,不能在全局函数或者static函数中使用
        // 在普通成员函数中,this是指向一个非const对象的const指针   Time* const this
        // 在const成员函数中,this是一个指向const对象的const指针 const Time* this
}
Time& Time::add_minute(int Minute)
{
	this->Minute += Minute;
	return *this;
}
Time& Time::add_second(int second)
{
	this->second += second;
	return *this;
}
int main()
{
    Time mytime;
    mytime.add_hour(3).add_minute(4).add_second(10);// 每次返回的是自己,所以可以一直重复掉用下去
    return 0;
}
static成员
void func()
{	
	static int abc = 5;// 局部静态变量,在计算机中,func在内存中分配一段内存,给abc分配一片静态内存,存储在静态存储区,每次调用func函数,abc分配的这块内存可保存上次修改后的内容
	// ... 
	abc = 9;
}
#inclue "stdafx.h"
#include "Time.h"

#include<iostream>
using namespace std;
extern int g_abc;// 外部的cpp文件也可以使用这个变量

static int s_abc;// 存储在静态存储区,系统初始值为0,如果放在全局位置,默认起始为本文件中

void f()
{
	cout<< g_abc <<endl;
}

​ static定义的类成员变量是属于整个类的,不管定义了多少对象,所有对象共有,某个对象修改了,所有对象都能看到

这种成员变量的应用方式有所区别:

类名::静态成员变量名

​ 成员函数前面也可以加static,加了static的成员函数已经不隶属于某一个对象了,而是隶属于某一个类的函数,调用时

类名::静态成员函数名

​ 静态成员函数和静态成员变量在头文件中并不会马上分配内存,所以声明时不能赋值

​ 示例:Time.h头文件

public:
	static int mystatic = 10;// 编译器会报错,因为还没有分配内存,不能赋值
	// 正确写法
	static int mystatic;
	static int funcstatic(int a);// 声明成员函数

​ 什么时候被分配空间呢,为了能够保证在调用任何函数之前这个静态成员已分配内存,可以在某一个.cpp文件开头定义,这样就能保证在调用任何函数之前,这个静态成员变量已经被初始化

​ 某个.cpp文件

#inclue "stdafx.h"
#include "Time.h"

#include<iostream>
using namespace std;

int Time::mystatic;// 可以不给初值,系统默认给0,定义时不需要使用static,必须保证值在一个.cpp文件中赋初值,否则会报错重定义
// 可以不通过对象,直接Time::mystatic使用,也可以通过对象调用的方式使用
与类相关的非成员函数
// 当某一个函数需要传入某一个类的对象,所以在定义该函数,必须将之放在类定义所在的头文件,但是这样会导致头文件过于臃肿,所以,可以在类所在的头文件里声明该函数,在用到它的地方定义包含该类所在的头文件并定义该函数

类内初始化

​ 在C++中,对于非静态成员变量,将其定义在头文件中并进行赋值是可以的,因为这些变量会被视为每个包含该头文件的编译单元的一部分。这种方式可以确保每个编译单元都有自己的实例。
​ 然而,对于静态成员变量,如果在头文件中进行初始化赋值,由于头文件可能会被多个编译单元包含,这将导致多个编译单元中都存在对同一静态成员变量的定义,从而违反了C++的 One Definition Rule(ODR)规则。ODR规则要求每个静态成员变量只能有一个定义。

class Time
{
    private:
        int Millisecond;
    	void initMillTime(int mls);
    	int Hour;
    	int Minute;
    	int Second;
    public:
        int Hour;
    	int Minute;
    	int Second;
    	void initTime(int tmphour, int tmpmin, int tmpsec);
    	Time(int tmphour, int tmpmin, int tmpsec);
};
Time::Time(int tmphour, int tmpmin, int tmpsec):Hour(2) ,Minute(3), Second(4) // 构造函数初始化列表
const成员变量的初始化
class Time
{
    private:
        int Millisecond;
    	void initMillTime(int mls);
    	int Hour;
    	int Minute;
    	int Second;
    	const int ctestvalue;// 定义了一个成员常量,这里如果不给初值,就会报错,下面通过构造函数·1给出
    public:
        int Hour;
    	int Minute;
    	int Second;
    	void initTime(int tmphour, int tmpmin, int tmpsec);
    	Time(int tmphour, int tmpmin, int tmpsec,const int constvalue);
};
Time::Time():ctestvalue(2){}// 系统在构造这个对象时候,就给ctestvalue赋予常量属性,后续如果在类中又赋初值,系统就会报错
默认构造函数

​ 没有参数的构造函数叫做默认构造函数,在类定义中,如果没有构造函数时,编译器就会隐式自动定义一个无参的构造函数,称为:合成的默认构造函数;这个合成,干了什么事呢?那什么情况下必须用自己要写的构造函数呢?

class S2
{
public:
	explicit S2();
};
class S1
{
public:
	explicit S1(int);
private:
	int a;
	int b;
	S2 c;
};
#include"S.h"
#include<iostream>
using nemsapce std;
S1::S1():S2(3)// 构造函数初始化列表给S1中创建的S2对象赋初值
{
	b = 3;	
}
int main()
{
	S2 d;// 这里创建S2对象,调用默认构造函数,由于类中已经定义了默认构造函数,所以系统会执行S1的默认构造函数,又S1中包含S2	构建的对象,所以也会执行S2的默认构造函数,但是S2的默认构造函数需要传参,
    // 所以如果执行系统合成的默认构造函数,编译器肯定会报错,像这种情况就必须使用用户自己创建的默认构造函数,编译器创建合成的构造函数的条件是:只要自己创建了构造函数,不管这个构造函数带几个参数
    // 编译器就一定不会合成的默认构造函数
	return 0;
}

​ 总结:默认构造函数就是不带参数的构造函数,如果我们不自己定义构造函数,系统就会为我们定义一个合成的默认构造函数,这个合成的构造函数我们看不见,没有初始化值的成员变量,合成的默认构造函数会赋予随机值。有些情况下,我们没办法是使用合成的默认构造函数,这时候就需要我们自己书写构造函数。

=default ; =delete;

​ =defalult的作用就是:编译器能够为我们自动生成函数体,可用来偷懒,但是它不许用在默认构造函数,如果带参数就不行,如下例就会报错:"Time2::Time2(int)"不是可默认为的特殊成员函数

class Time2
{
	Time2(int){};
	Time2(int) = default;
};

​ 只能用在默认构造函数里,这样就不会报错(普通函数也不能这样写,非特殊的函数都不可以使用 = default; ):

class Time2
{
	Time2(){};
	Time2() = default;
};

​ = delete; 让程序员显式的禁用某个函数

Time2::Time2() = delete;// 执行这句后,因为没有了不带参数的构造函数,创建对象就会失败
  • 26
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值