C++基础知识合集
提取码:maku
C++ 初体验
学语言首先肯定得先学语法了,一句结构完整的话在语文和英语中是存在主谓宾的,那么 C++ 它的基本构成又是什么呢?每个 C++ 程序可以包含一个或者多个函数,其中一个必须命名为 main 。操作系统通过调用 main 来运行 C++ 程序。以下就是一个非常简单的 main 函数,它什么也不干,只是返回给系统一个值:
int main()
{
return 0;
}
一个函数由四个部分组成,分别是:返回类型(return type)、函数名(function name)、一个包含括号的形参数列表(parameter list)以及函数体,这就像一座房间一样,返回值就是从房间里出去的人、参数列表就是进入房间的人、房间的主体结构就像函数体一样,函数名就就是房子的大门,我们可以允许返回值和参数列表为 void(空),但是必须要有函数体和函数名,就像一间房它可以不住人,但它的基本结构和门得有,这才符合它的功能。每个语句都要以分号结尾,表示这句话结束了,同时函数体需要用大括号包含起来,这就像房子里的房间需要间隔一样,每句话之间也要有分明的标记,房子的外墙就和括号一样,把房子里的东西都包括起来,表明这里面的所有东西都属于这个房子。
一个程序写好了开始编译运行它,毕竟 C++ 语言是高级语言,需要将它变成机器能识别的语言才能让计算机去完成你想让程序做的事。程序运行一共有预处理、编译、汇编、链接四个过程。在了解了一个程序的结构后,我们总不能写啥也不干的函数啊,要想让程序做点事,就像往一个空房子里添置家具一样,你得把你想要做的事写在函数体里。比如,在我大一的时候写的第一个程序,我想也应该是每个程序语言初学者都会写的第一个程序,输出“hello world !”
#include<iostream>
int main()
{
std::cout << "hello world !" << std::end;
return 0;
}
好了,这么一步我们就完成了添置家具的事了,总算是让它做了一点事了,其中 iostream 是个包含输入 cin 类和输出 cout 类的头文件,std 是命名空间,在这个头文件中存在多个同名的类,需要用命名空间来将同名的类区分,然后使用的时候就引用自己需要的那个类就可以了。上面我们使用 cout 这个输出类完成了我们让程序打印 hello world ! 的操作。那我们怎么让它完成输入的操作呢?此时就是 cin 这个类上场的时候了,我们从键盘上敲入一个数存入程序的变量中,变量是有一定大小存储空间的,用来保存我们输入的数,比如我们写一个输入两个数 num1 和 num2 ,输出它们的和的程序。
#include<iostream>
int main()
{
int num1,num2;
std::cin >> num1 >> num2;
std::cout << num1 + num2 << std::endl;
//std::cout << "hello world !" << std::end;
return 0;
}
以上,我们完成了让程序输出和输出的操作了,同时我在程序中注释了输出 hello world!的语句。在 C++ 中,分单行注释和多行注释,上面是注释一行的写法,下面这个是注释多行的写法。
#include<iostream>
int main()
{
/*
std::cin >> num1 >> num2;
std::cout << num1 + num2 << std::endl;
std::cout << "hello world !" << std::end;
*/
return 0;
}
注释是个很好的习惯,可以帮助我们理解自己的代码,有时候代码太长了或者有的时候某几行代码很难想出来,一旦写出来了加上注释是很有必要的,有利于以后回头看的时候快速地了解函数写的是用来实现什么功能的。
以上我们了解了一个程序的组成,函数的基本结构,函数是怎么样输入和输出的,知识点比较简单,主要是对 C++ 程序有个初入的认识,慢慢地会了解得越来越深入,你可能会发现,原来它不是那么简单了!
控制语句初体验
有时候,我们要输出的操作可能几十成百上千次,这样我们再去一句话一句话地写就有些不现实了,不仅很费时间,而且大量重复的代码也会影响代码的美观,如果我们可以让程序循环地去实现我们的输出操作就完美了,那么有没有能帮助我们解决这个问题的方法呢?答案自然是肯定的了,循环控制语句之while语句,while这个英文的意思是当什么什么的时候就如何,放在程序中我们仍然可以这么理解的,当满足一定的条件的时候,我们就去做一件事,如果不满足这个条件了,就跳出循环体即可。
#include<iostream>
int main()
{
int count = 10;
while(count > 0)
{
std::cout<<"hello world!"<< std::endl;
count--; //等价于 count = count - 1;
}
return 0;
}
以上就是一个简单while控制语句,当count小于10的时候就打印hello world!,打印一次,count就减一,当count不满足大于0的要求时,就退出while循环不再继续打印了,此时我们就完成了打印10次hello world!的操作了。
看while语句的时候是否会有这样的疑问呢?控制语句里的count和count–为啥不和它一起存在于while中,我只想要一个简洁的控制语句,在控制语句中只放打印语句,同时控制语句以外啥都不要,这种要求当然也是可以被满足的,那就到了for语句的出场时候了。
#include<iostream>
int main()
{
for(int count = 10; count > 0; count--)
{
std::cout<<"hello world!"<< std::endl;
}
return 0;
}
以上就是一个简单for控制语句,实现了和while一样的控制效果,第一句话是控制变量的定义和初始化,第二句话是判断它是否满足控制条件,第三句话是执行完打印操作后自减一,它们的执行顺序分别是,先执行定义和初始化,然后执行判断,如果满足条件就执行循环体,然后执行count自减一,继续执行判断,循环这个过程直到判断不满足条件后退出,这里的第三句话不用分号结尾,括号已经标识了它要在这里结尾,而中间的两个分号必须写,不然区分不了是三句话。不管怎么说,for语句将控制条件的定义、初始化、判断以及自减操作继承到了一个括号里,这样看上去就比while简洁很多了,算是一个很大的优势了。
同样的我们控制了循环的输出,也可以试试如何循环输入,用一个变量保存输入的数,每次都将输入的数累加,最后输出累加的和,比如下面这个程序,这里有个新的写法 sum += num; 等价于 sum = sum + num;
#include<iostream>
int main()
{
int sum = 0;
int num = 0;
for(int count = 10; count > 0; count--)
{
std::cin >> num;
sum += num;
}
std::cout << sum << std::endl;
return 0;
}
以上我们初步认识了循环控制语句while和for的基本用法,显然易见的是它们让程序变得更加的复杂,但同时可以满足我们更多的需求了,正所谓有利就有弊,如果我们可以克服这些弊端,那么对C++的认识和编写代码的能力肯定会有进一步的提高。
再探控制语句
控制语句中不仅仅是循环控制,现实生活中面临着种种选择,就像你站在岔路口,你会根据你要去的地方选择一个方向,既然计算机是用来辅助解决人们面临的问题,那么,当然也要有能实现这种选择问题的办法了,其中之一就是 if 语句,计算机判断一个条件是否满足,如果满足则执行,不满足则不执行。
#include<iostream>
using namespace std;
int main()
{
int sum1 = 0;
int sum2 = 0;
for(int i = 0; i <= 10; i++)
{
if(i % 2 == 0)
sum1 += i;
else
sum2 += i;
}
cout<<sum1<<endl;
cout<<sum2<<endl;
}
以上,我们实现了一个 if 和 else 语句,当 i 为奇数累加到 sum2 ,否则累加到 sum1 ,最后输出了 1–10 之间的偶数和和奇数和,else 表示如果我不执行if里的语句,那么我就执行else里的语句,类似我们做二选一的操作。同时在 main 上面我添加了一个 using namepsace std; 这表示我们要用 std 里面的类,前面我们说到了头文件里会有很多同名的类,我们要用一个命名空间将这些同名的类区分开,最后我们添加我们要用的类所在的命名空间,这样就可以避免每次写输入输出都加上引用了。
可以看到,这个程序把之前的循环控制,输出也加上了,程序逻辑变得更加复杂,但同时也帮我们解决更多复杂的问题,随着程序慢慢地复杂起来,你会发现它和生活越来越紧密和贴切,也许这也是计算机的魅力之一,你可以通过计算机去解决生活中一些繁琐棘手的事,当你渐渐喜欢上这种感觉,喜欢上这门语言,你就会发现,相比与人交流,它显得更加纯粹和真诚。
变量
变量的申明、定义、初始化
变量其实只不过是程序可操作的存储区的名称。C++ 中每个变量都有指定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。(变量名字本身不占据任何资源,但是可以通过它访问到它所标识内存空间的数据,这个是直接访问到它的值,和后续的指针不同,指针是间接访问内存空间的数据)
变量的名称可以由字母、数字和下划线字符组成。它必须以字母或下划线开头。大写字母和小写字母是不同的,因为 C++ 是大小写敏感的。
变量的申明和定义,在 C++ 中变量只有先申明了后续才能使用,俗话说,巧妇难为无米之炊,想用一个变量的时候,它必然是已经存在的,不然就无法使用,申明变量之后,最好对变量进行初始化,也就是对变量赋值。虽然C++对各种变量都有默认的初值,但是申明并初始化是一个很好的 coding 习惯。有时候可以帮助我们避免一些问题,尤其是后面对指针的使用。
#include <iostream>
using namespace std;
// 变量声明
extern int a, b,c;
extern float f;
int main ()
{
// 变量定义
int a, b;
int c;
float f;
// 实际初始化
a = 10;
b = 20;
c = a + b;
cout << c << endl ;
f = 70.0/3.0;
cout << f << endl ;
return 0;
}
上面的 extren 是C++ 的关键字,表示变量在别处已经定义,此时是在申明它,意味着将在这个文件中使用,当然这里我们的定义也在同一个文件中,但是在其他的文件中对变量进行定义,只要这里写了申明也是可以使用的,这就实现了文件之间的数据共享。但是如果没有这个申明,变量的定义等价于申明并且定义,比如:
#include <iostream>
using namespace std;
int main()
{
int i,j ,k; //申明并定义
//对变量进行初始化
i = 1;
j = 2;
k = 3;
//还可以申明定义初始化都写在一起
int i = 1, j = 2, k = 3;
return 0;
}
左值和右值
- 左值(lvalue):指向内存位置的表达式称为左值,通俗来说就是可以被赋值,可以出现在等号的左右边
- 右值(rvalue):存储在内存某些地址中的数值,不能被赋值,只能出现在等号的右边,不能出现在左边
int i = 20; //i是左值,20是右值,可以对i赋值,但不能对20赋值
变量就和字面意思一样,它随着程序的运行是可以发生改变的,有可能你初值赋为 0 后面又会变成别的数,比如 sum 一开始等于 0,后续它等于两个数 num1 和 num2 的和,这种就称之为变量。那么既然有变量就会有不变量了,不然变的意义就不大了。下次接着说不变的量的特点。
常量
和变量不同的是,常量是在程序运行期间不会改变的量,它可以是任何的基本数据类型,可分为整型数字、浮点数字、字符、字符串和布尔值。常量就像常规的变量,只不过常量在定义之后不能进行改变。
整数常量有十进制,也就是日常我们使用的形式;八进制是用0开头;十六进制是用0x开头的常量。如果整数常量带一个后缀U则表示符号, L 则表示长整数。后缀可以是大写,也可以是小写,顺序任意。下面列举几个整数常量的实例:
212 // 合法的
215u // 合法的
0xFeeL // 合法的
078 // 非法的:8 不是八进制的数字
032UU // 非法的:不能重复后缀
浮点常量由整数部分、小数点、小数部分和指数部分组成。小数形式或者指数形式都可以表示浮点常量。如果是小数形式,则必须包含整数部分、小数部分,或都包含。如果指数形式, 则必须包含小数点、指数,或都包含。带符号的指数通过 e 或 E 表示的。比如下面的例子:
3.14 // 合法的
3141E-5L // 合法的
5E // 非法的:不完整的指数
2f // 非法的:没有小数或指数
.e5 // 非法的:缺少整数或分数
布尔常量共有两个,属于 C++ 的关键字:true 值代表真;false 值代表假。我们不应把 true 的值看成 1,把 false 的值看成 0。
字符常量是括在单引号中。如果常量以 L(仅当大写时)开头,则表示它是一个宽字符常量(例如 L’x’),此时它必须存储在 wchar_t 类型的变量中。否则,它就是一个窄字符常量(例如 ‘x’),此时它可以存储在 char 类型的简单变量中。
字符常量可以是一个普通的字符(例如 ‘x’)、一个转义序列(例如 ‘\t’),或一个通用的字符(例如 ‘\u02C0’)。
在 C++ 中,有一些特定的字符,当它们前面有反斜杠时,它们就具有特殊的含义,被用来表示如换行符(\n)或制表符(\t)等。
#include <iostream>
using namespace std;
int main()
{
cout << "Hello\tWorld\n\n";
return 0;
}
//上面结果输出 Hello world,还有两个换行
字符串字面值或常量是括在双引号 “” 中的。一个字符串包含类似于字符常量的字符:普通的字符、转义序列和通用的字符。可以使用空格做分隔符,把一个很长的字符串常量进行分行。下面的实例显示了一些字符串常量。下面这三种形式所显示的字符串是相同的。
"hello, dear" //连接在一起
"hello, \ //换行分开写
dear"
"hello, " "d" "ear" //分成三个部分
定义常量,在 C++ 中有两种简单的定义常量方式:1.使用 #define 预处理;2.使用 const 关键字
#define identfier value 没有分号,预处理不用进行类型检查,在编译之前完成
const type variable = value; 关键字+类型名+变量名+初始化操作;要进行类型检查,在编译期完成
include<iostream>
using namespce std;
#define PI 3.14
int main()
{
int r = 3;
float = area;
area = PI * r * r;
cout << area << endl;
return 0;
}
include<iostream>
using namespce std;
int main()
{
const float PI 3.14;
int r = 3;
float = area;
area = PI * r * r;
cout << area << endl;
return 0;
}
以上就是各种的常量形式,关键一点就是常量在程序运行期间不会改变,其他的就是对各种常量形式的记忆了。
数据类型
基本内置类型
用编程语言进行编程时,需要用到各种变量来存储各种信息。变量保留的是它所存储的值的内存位置。这意味着,当我们创建一个变量时,就会在内存中保留一些空间。但是我们不可能用很小的内存去放很大的数,就和房子是一样,三人间的房子,五六个人显然是住不下的,同样,一个人也没必要住三人间,这样显得很多地方都没用上被浪费掉。对于计算机而言,它的内存资源是有限的,应当是你需要多大的空间,它就调配多大的内存资源给你用。这样我们就需要各种各样的数据类型(比如字符型、宽字符型、整型、浮点型、双浮点型、布尔型等)的信息,操作系统会根据变量的数据类型,来分配内存和决定在保留内存中存储什么。以下是C++七种基本的数据类型:
类型 关键字
布尔型 bool
字符型 char
整型 int
浮点型 float
双浮点型 double
无类型 void
宽字符型 wchar_t
其中 wchar_t 的由来是:
typedef short int wchar_t;
也就是说 wchar_t 等价与 short int ,其中 typedef 意思是类型重定义,相当于为类型取外号。
然后一些基本的数据类型前面还可能有一些类型修饰符进行修饰:
- signed
- unsigned
- short
- long
其中前面两个是修饰有没有符号,也就是正负号,因为符号也要占用一位,所以有符号的数值绝对值大小只有无符号的一半,后两个是用来修饰整型数,short int 占位是 int 的一半,long int 占位是 int 的两倍,其中 int 占四个字节。float 单精度浮点型占位四个字节,double 双精度占位八个字节,这两个是用来表示小数的。
枚举类型
枚举类型(enumeration)是 C++ 中的一种派生数据类型,它是由用户定义的若干枚举常量的集合。如果一个变量只有几种可能的值,可以定义为枚举(enumeration)类型。所谓”枚举”是指将变量的值一一列举出来,变量的值只能在列举出来的值的范围内。创建枚举,需要使用关键字 enum。枚举类型的一般形式为:
enum 枚举名称{ 标识符1,标识符2,标识符3...}枚举变量(标识符的具体值是整型常量)
eg. enum color{red, green, blue} col; //默认从0开始依次递增1
col = blue; //那么此时的col = 2;
eg. enum color{red = 3, green, blue} col; //此时从3开始以依次递增1
col = blue; //那么此时的col = 5;
好了,基本的数据类型已经介绍完了,当然想处理生活中的各类问题,这些显然是不够的,后续我们还会自定义更多的数据类型来满足我们的不同需要。
复合类型
复合类型是指基于其他类型定义的变量,通常有引用和指针;
引用:
简单的理解就是为一个对象起别名,就相当于小时候经常给同学起外号一样,是为一个已经存在的人起外号,那么也就是说引用变量必须初始化,且初始化为已经存在的对象。引用的使用方法是 :
& + 引用名 = 被引用的对象
比如:
int a = 2;
int &b = a; //合法
int &c = 2; //非法,2是右值,引用这里指的是左值对象,右值引用前面说过,是 &&d = 2;
int &d; //非法,引起申明时就要初始化
int &e = a; //合法,可以为一个对象起多个别名
int _a = 3;
&b = _a ; //非法,引起申明时就要初始化,同一个引用只能绑定一个对象,且不能改变绑定的对象
指针:通俗地理解就是使用一个指针变量存放它所指向对象的地址,通过这个地址来间接访问对象,使用方法如下:
int* p; //指针本身也是个变量,可以只申明不定义
int* p1 = nullptr; //指针变量可以指向为空
int a;
p = &a; //指针变量可以指向一个未初始化的变量
a = 1;
cout<<*p<<endl; //此时指针变量通过解引用访问a地址的值 p = 1;
int b = 2;
p = &b; //指针变量可以改变它的指向,此时 p = 2;
&和*的用法和含义
int a = 1;
int &b = a; //此时&表示引用
int* p = &a; //*表示申明指针变量,&表示取对象的地址
*p = 2; //*表示解引用,p指向的值为2
int c = 3;
p = &c; //此时p表示的是地址变量,存放c的地址
// *作为申明出现是表示申明的是指针变量,其他时候表示指向的值
// &作为申明出现表示申明的是引用变量,其他时候表示的是对象的地址
引用和指针的区别:
1.引用是对象的别名,对对象的访问属于直接访问,而指针存放的是对象的地址,需要通过解引用间接访问对象
2.引用申明的时候需要初始化,且必须初始化为已存在的左值对象,不能在绑定别的对象,而指针本身就是个变量,它可以申明为空,也可以初始化,同时还可以改变指向
3.对引用的sizeof操作求的是原对象的大小,而对指针的sizeof操作求的是指针变量本身的大小
4.引用只能有一级,而指针可以多级(**p,二维数组)
5.引用本身不占内存,指针本身还需要占用一定大小的内存空间(因为指针本身也是个存放地址的变量,它需要一个空间存放地址,而引用只需要指向原对象的地址,它本身没有要存的内容)
自定义数据类型
通俗地理解就是将一组相关的数据元素组织在一起,然后使用它们的方法和策略,比如我们有一个学生的信息,包括学生的名字,学号,课程信息以及课程成绩等,那么我们就可以把学生的各种信息用不同的变量类型存放然后作为一个整体来使用。
#include <iostream>
using namespace std;
#include<string>
//对字符串的一些操作赋值和输出,需要的加上string的头文件
struct Student {
string name; //名字
string nums; //学号
string course; //课程
int grades; //成绩
}; //结构体也是变量的一种形式,只不过它封装了多个数据类型,作为变量的申明,在申明变量后需要加上分号
int main()
{
//申明结构体后,通过.操作符访问结构体内的变量
Student stu1,stu2,stu3;
stu1.name = "张三";
stu1.nums = "001";
stu1.course = "C++";
stu1.grades = 90;
stu2.name = "李四";
stu2.nums = "002";
stu2.course = "C++";
stu2.grades = 85;
stu3.name = "王五";
stu3.nums = "003";
stu3.course = "C++";
stu3.grades = 95;
//输入张三的个人信息
cout << stu1.name << " " << stu1.nums << " " << stu1.course << " " << stu1.grades << endl;
//同样可以从外面读入每个学生的个人信息
//注意在输入的过程中,正确地对应每个变量的数据类型
Student stu4, stu5, stu6;
cin >> stu4.name >> stu4.nums >> stu4.course >> stu4.grades;
cin >> stu5.name >> stu5.nums >> stu5.course >> stu5.grades;
cin >> stu6.name >> stu6.nums >> stu6.course >> stu6.grades;
//判断学生4和5的成绩谁更高,输出他的名字
if (stu4.grades > stu5.grades)
cout << stu4.name << endl;
else
cout << stu5.name << endl;
}
标准库类型string
1.定义和初始化string对象
对象的初始化由对象本身决定,它可能有多种初始化的方式,根据不同的初始化写法使用相应的方式
#inlucde<iostream>
using namespace std;
#include<string>
int main()
{
stirng s1; //默认的初始化,s1是一个空串
string s2(s1); //s2是s1的副本
string s3 = s1; //等价与s2(s1),s3是s1的副本
srting s4 = "string"; //s4是“string”的副本
string s5("string"); //等价与s4 = "string",s5是“string”的副本
string s6(n, 'c'); //s6初始化为n个连续的‘c’字符
}
直接初始化和拷贝初始化
string s1 = "value"; //拷贝初始化
string s2(s1); //直接初始化
string s3(3, 'a'); //直接初始化
string s4 = string(6, 'a') //拷贝初始化
//等价于
string tmp = string(6, 'a');
s4 = tmp; //多值初始化采用直接初始化更高效
string对象上的操作
一个对象除了申明定义之外,还应该有针对这个对象本身的一些操作,比如输入输出,求string对象的大小,赋值和比较等
string s;
cin>>s;
cou<<s;
string s;
getline(cin, s); //将输入流中的数据赋值给s
s.empty(); //判断s是否为空串,返回true或false
s.size(); //返回s的长度(字符个数)
string s1 = "hello";
string s2 = "world";
s = s1 + s2; //s为s1和s2拼接的结果
s = s1; //将s1替代s中原来到字符
s1 == s2; //判断s1和s2是否相等,字符完全一样才相等否则不等
s1 != s2;
//另外还有<,>,<=,>=根据字符在字典中的顺序比较大小,大小写不一样 a和A不等
//字符串相加的时候需要格外注意,字符串字面值不能进行相加
string s3 = "hello" + "world"; //非法
string s3 = s3 + "hello" + "world"; //合法,等式的右侧必须有一个string对象
s3 = "C++" + s3;
//处理每个字符,一般的for循环语句
for (int i = 0; i < s3.size(); i++)
cout << s3[i] << " ";
//范围for语句,可以不用写控制循环的变量,但是在需要用到字符的索引时这种写法就不适用了
for (char ch : s3)
cout << ch << " ";
标准库函数vector
vector表示对象的集合,其中所有对象的类型都相同。每个对象都有一个与之对应的索引,根据这个索引可以访问到这个对象。
定义和初始化vector对象
vector<int> iVec; //定义了一个存放int类型的容器
vector<Student> stuVec; //定义了一个存放Student类型的容器
vector<string> strVec; //定义了一个存放string类型的容器
//定义和初始化
vector<int> vec; //定义了一个存放int类型的空容器
vector<int> vec1(vec); //vec1中包含了vec中所有的元素
vector<int> vec2 = vec; //vec2中包含了vec中所有的元素
vector<int> vec3(10, 1);//vec3中包含了10个1
vector<int> vec4(10); //vec4中有10个0,不设置元素值的时候默认为0
vector<int> vec5{ 1,2,3,4,5, }; //vec5包含1,2,3,4,5
vector<int> vec6 = { 1,2,3,4,5 }; //vec6包含1,2,3,4,5等价与上一种形式
//()和{}的区别
vector<int> v1(10); //10个0
vector<int> v2{ 10 }; //1个10
vector<int> v3(10, 1); //10个1
vector<int> v4{ 1,10 }; //1个1和1个10
//向vector中添加元素
vector<int> v;
//每次都从尾部添加元素 v = {1,2,3,4,5,6,7,8,9}
for (int i = 0; i < 10; i++)
v.push_back(i);
//有关vector容量的设置,如果知道vector需要装多个个元素在定义的时候就设置大小,因为vector在添加元素的时候如果容量不够会扩容,扩容的操作很耗费时间
//vector的其他操作
v.empty(); //判断v是否为空
v.size(); //判断v中元素的个数
cout << v[6]; //输出索引为6的元素
v1 == v2; //判断v1和v2所有对应位置的元素是否相等
v1 != v2; //判断是否存在对应位置不等的元素
//<,> <=, >= //根据字典序进行比较
//注意:使用索引访问元素的时候需要判断索引是否越界
// 使用一般的for语句
for (int i = 0; i < v.size(); i++)
cout << v[i] << endl;
//使用范围for遍历
for (auto it : v)
cout << it << endl;
//使用迭代器进行访问,begin()返回容器的首位元素的迭代器,end()返回最后一个元素下一位置的迭代器
for (auto it = v.begin(); it != v.end(); it++)
cout << *it << endl;
一维数组
与标准类型库vector类似,数组也是用来存放同种类型的对象的,只不过这些对象没有名字,只能通过它们所在的位置进行访问(下标索引值),但是在性能和灵活性上又与vector有所不同,数组在申明的时候就要确定数组的大小,也就是存放对象的个数,数组不能随意增加元素(有大小限制,不能越界)
注意:如果不知道数组的大小就使用vector
//数组的定义和初始化
//数组的定义形式为: 数组类型 数组名[size];
int array[10]; //申明了一个大小为10的int数组,数组的元素是int变量
int* p[10]; //申明了也给大小为10的指针数组,数组的元素是指针变量
int a = 10; //int arr[a] 非法,数组的大小在编译期确定,不能是变量
const int b = 10; //#define c 10
int* arrb[b]; //采用const修饰或者define定义申明常量,作为数组的大小
constexpr int c = 10; //采用constexpr关键字申明常量表达式,作为数组的大小
int a1[3] = { 0,1,2 }; //初始化的时候给定数组的大小
int a2[] = { 0,1,2 }; //初始化的时候根据元素数量确定数组的大小
int a3[5] = { 0,1,2 }; //数组的元素为0,1,2,0,0,未初始化的位置默认为0
string a4[3] = { "hello", "world" }; //数组的元素为{"hello", "world",""},为初始化的位置默认为空串
//int a5[2] = { 0,1,2 };非法,数组最多存放2个元素
//字符数组
char ch1[] = { 'a','b' }; //默认在数组的后面添加‘\0’,数组大小为3,字符数组表示的字符串长度为2
char ch2[] = { 'a','b','\0' }; //显示申明了字符串的结尾符,数组大小为3,字符数组表示的字符串长度为2
char ch3[] = "C++"; //默认在数组的后面添加‘\0’,数组大小为4,字符数组表示的字符串长度为3
//char ch4[3] = "C+++"; 非法,数组的大小为3,而存在c++时会再添加一个空字符
//数组不允许拷贝和赋值
int a[] = { 1,2,3 };
/*
int b = a; 非法,不允许用一个数组初始化另一个数组
b = a; 非法,不允许一个数组赋值非另一个数组
*/
//复杂数组的理解
int* ptr[10]; //含有10个整型指针的变量
//int &arr[10] 非法,不存在引用的数组
int arr[10];
int(*Parray)[10] = &arr; //Parray指向了含有10个整型数的数组(指针变量指向数组变量的地址)
int(&arrRef)[10] = arr; //arrRef引用了含有10个整型数的数组(引用变量是数组变量的别名)
//数组的访问,一般for循环
for (int i = 0; i < 10; i++)
cout << array[i] << " ";
//范围for语句
for (auto it : array)
cout << it << " ";
//数组和指针
string str[] = { "1", "2","3" };
string* pstr = str; //数组名指向的是数组的首位地址(第一位元素的地址)
pstr = &str[0]; //指向数组中的第一个元素的地址
string *res1 = str + 1; //指向数组中的第二个元素
string *res2 = pstr + 2; //指向数组中的第三个元素
string* r1 = str + 3; //指向数组最后一个元素的下一位,位置的值未定义,不要解引用
string* r2 = str + 5; //指向非法的内存空间,arr只有三个元素
int an[] = { 0,1,2,3,4 };
int _a = an[1]; //使用索引访问数组中的元素,_a = 1
int* pt1 = an;
int _b = *(pt1 + 1); //使用指针访问数组中的元素, _b = 1
int* pt2 = &an[1];
int _c = *pt2; //pt2指向数组中第二个元素, _c = 1
int _d = pt1[-1]; //指向第一个元素,_d = 0
//数组的索引可以处理负数,而vector的索引值只能是无符号数,非负数
多维数组
多维数组本质上是数组的数组,也就是说数组的元素也是数组,了解二维数组基本上就可以了,再高的维度也是一样的道理。
#pragma once
#include<iostream>
using namespace std;
void mutilArray()
{
//多维数组定义和初始化
int a1[3][4]; //大小为3的数组,每个元素都是含有4个整数的数组
int a2[2][3][4] = { 0 }; //大小为2的数组,每个元素都是大小为3的数组,这些数组的元素是含有4个整数的数组
//二维数组的第一个维度称为行,第二个维度称为列
int a3[3][4] = { //三个元素,每个都是含有四个整型数的数组
0,1,2,3, //第一行的元素
4,5,6,7, //第二行的元素
8,9,10,11 }; //第三行的元素
int a4[3][4] = { {0,1,2,3},{4,5,6,7},{8,9,10,11} }; //数组内部可以用括号分割不同的行,当然也可以不用,他会根据元素个数自行计算
int a5[3][4] = { {0},{4},{8} }; //每一行的首位元素被初始化了,而其他的元素都被 初始化为0
int a6[3][4] = { 0,1,2,3 }; //只初始化了第一行的数据,其他位置被初始化为0
int a7[][4] = { 0,1,2,3 }; //多维数组可以省略第一个参数
int a8[][3][4] = { 0,1,2,3 }; //只能省略第一个,除了第一个以外,其他的大小都要指定
//多维数组下表的引用
a1[2][3] = a2[0][0][0]; //用a2数组中第一个元素为a1的最后一个元素赋值
int(&row)[4] = a1[1]; //row指向a1的第二个含有四个元素的数组上
//多维数组输出,一般情况
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
cout << a1[i][j] << " ";
cout << endl;
}
//使用范围for语句
for (auto &p : a1) //为了避免p被转换成指针类型,必须使用它的引用
{
for (auto q : p)
cout << q << " ";
}
//使用指针访问
for (auto p = a1; p != a1 + 3; p++) //auto 识别p的类型为指针类型,指向a1中的每行的首地址
{
for (auto q = *p; q != *p + 3; q++) //auto 识别q为p的首地址,指向p所指向这一行的所有元素(这里要对p进行解引用,这样访问的才是当前行的元素而不是元素的地址)
cout << *q << " ";
cout << endl;
}
//使用begin和end
for (auto p = begin(a1); p != end(a1); p++)
{
for(auto q = begin(*p); q != end(*p); q++)
cout << *q << " ";
cout << endl;
}
//指针和多维数组
//int* p[4]; 含有四个指针变量的数组
//int (*p)[4]; 指向一个含有四个整数的数组
}
初识函数
前面都是将所要实现的功能写在main函数,但是随着代码越来越多,main函数的代码就显得很复杂不易于理解,当我们要写的需求可以划分成多个功能模块时,可以把每个功能的实现写在自定义的函数中,在需要完成该功能的时候只需要调用函数名就可以了,这种写法不仅让代码的更加易于理解,同时在一个功能重复使用的时候还可以实现代码的复用。
和mian函数一样,自定义的函数结构也包括:返回值类型,函数名,函数参数列表,函数体四个部分组成,其中参数数量可以为0或者多个,多个参数中用逗号分割,函数体用花括号包含,最后通过函数名 + 参数 实现函数的调用。
/*
函数会为它的参数进行初始化,即先定义一个变量n,然后将传入的实参的值
对n变量进行初始化,最后使用形参进行运算得到最后结果。也就是说,此时的
实参是形参的副本,形参的变化不会导致实参的变化,他们只是不同的地址
里存放的两个相同的值
*/
//eg:实现n的阶乘
int func(int n)
{
int result = 1;
for (int i = 2; i <= n; i++)
result = result * i;
return result;
}
//函数申明
int f1(); //隐式无参数的函数
int f2(void); //显示无参数的函数
//int f3(int a, b); 非法,每个参数都必须申明类型即便类型是一样的
int f4(int a, int b); //正确
//int f5(int a, int a) 非法,参数必须是不同命的
局部对象
在C++中,名字有作用域,对象有生命周期
- 作用域:即对象能在程序中被访问的范围
- 生存周期:对象从创建到销毁的时间
//前置申明
int f5();
void f6()
{
cout << f5() << endl; //f5()前置申明了,可以正确使用
cout << func(5) << endl; //正确,func在f6之前有申明定义
}
int count = 5; //全局变量,整个程序中都可以使用
int f5()
{
/*
函数的局部变量,只能在这个函数体中使用,count采用就近原则,
函数体使用的函数内申明的count,隐藏了全局变量
*/
int count = 10;
//i是for循环语句中申明的,只能在for语句作用的循环体中使用
for (int i = 0; i <= count; i++)
{
cout << i << endl;
}
//cout << i << endl; 非法,变量未定义
/*
局部静态变量,存在于整个程序,但是只能在当前申明的函数中使用,
未初始化时则默认初始化为0
*/
static int a = 0;
cout << a << endl;
return a;
}
函数的参数传递
- 传递值:传递时形参将会生成新的变量初始化为实参的值,形参的改变不会影响实参
- 传递引用:相当于形参是实参的别名,形参的改变会导致实参的改变
- 传递指针,传递时形参将会生成新的指针变量初始化为实参的值,形参指向的地址改变不会影响实参,但是在不改变指向的时候形参指向的值改变会导致实参指向值的改变
void swap1(int a, int b)
{
int c = a;
a = b;
b = c;
}
void swap2(int& a, int& b)
{
int c = a;
a = b;
b = c;
}
void swap3(int* a, int* b)
{
int c = *a;
*a = *b;
*b = c;
}
void f7(int* a)
{
int c = 10;
a = &c;
}
void test()
{
int n = 5;
//此时将函数参数n赋值为5,带入到函数体中运算,最后返回结果
cout << "5! = " << func(n) << endl;
/*
值得注意的是,在传递实参的时候,会对函数参数
列表进行匹配,如果参数的个数或者类型不同则不会进行计算(即匹配不成功)
*/
//func(); 参数个数不够
//func(1,2) 参数数量太多
//fun("hello") 参数类型不匹配
//fun(5.2) 参数进行类型转换,float转int,5.2变成5然后进行计算即等价于 fun(5)
int a = 3, b = 5;
swap1(a, b);
cout << a << " " << b << endl; //值传递不改变实参的值,a = 3, b = 5
swap2(a, b);
cout << a << " " << b << endl; //引用传递交换了实参的值,a = 5, b = 3
int* pa = &a;
int* pb = &b;
swap3(pa, pb); //指针传递交换了实参的值,a = 3, b = 5
cout << a << " " << b << endl;
cout << "未改变指向时a的值" << *pa << endl; //a = 5
f7(pa);
cout << "改变指向后a的值" << *pa << endl; //a = 5
//改变指针指向的时候,原指针的指向并没有发生改变,指向的值不变,a值不发生改变
}
一维数组名和指针作为函数参数
传递数组名
参数列表为数组名和数组的大小,arry[]表示传递的是个指向数组首地址的指针,但同时它还指向数组的第一个元素
void PrintArray(int arry[], int n)
{
for (int i = 0; i < n; i++)
cout << arry[i] << " ";
cout << endl;
}
传递指针
参数列表为指针名和数组的大小,p表示传递的是个指向数组首地址的指针
void PrintPtr(int *p, int n)
{
for (int i = 0; i < n; i++)
cout << p[i] << " ";
cout << endl;
}
分别调用这两个函数
//函数传递数组和指针
int arry[5] = { 1,2,3,4,5 };
cout << "传递数组名" << endl;
PrintArray(arry, 5);
int *p = new int[5];
for (int i = 0;i<5;i++)
{
p[i] = i;
}
cout << "传递指针" << endl;
PrintPtr(p, 5);
运行程序结果如下:
void PrintPtr(int *p, int n);
void PrintArray(int arry[], int n);
如上的两个函数原型相同,arry[]和*p都是传递数组的首地址,因此在任一函数中传递指针和数组名都可以
PrintPtr(arry, int n);
PrintArray(p, int n);
//函数传递数组和指针
int arry[5] = { 1,2,3,4,5 };
int *p = new int[5];
for (int i = 0; i < 5; i++)
{
p[i] = i;
}
cout << "传递数组名" << endl;
PrintArray(p, 5);
cout << "传递指针" << endl;
PrintPtr(arry, 5);
二维数组名和指针作为函数参数
二维数组名
函数原型为
void PrintAr2(int arry[][3], int row);
第一个参数表示传递的二维数组的指针,并规定了其列的数量,第二个参数规定了二维数组行的数量
void PrintAr2(int arry[][3], int row)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < 3; j++)
cout << arry[i][j] << " ";
cout << endl;
}
}
指针
函数原型为
void PrintPtr(int **p, int m, int n)
第一个参数表示它是个二维指针,后面两个参数代表二维数组的行和列
void PrintPtr(int **p, int m, int n)
{
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
cout << p[i][j] << " ";
cout << endl;
}
}
函数调用
int ar2[2][3] = { {1,2,3},{4,5,6} };
int **p = new int*[2];;
for (int i = 0; i < 2; i++)
p[i] = new int[3];
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < 3; j++)
p[i][j] = 0;
}
cout << "传递数组名" << endl;
PrintAr2(ar2, 2);
cout << "传递指针" << endl;
PrintPtr(p,2,3);
二维指针在使用之前要手动分配内存并赋值,分别调用后的结果如下所示:
函数的参数传递(值,指针,引用)
- 传递值,传递时形参将会生成新的变量初始化为实参的值,形参的改变不会影响实参
- 传递引用,相当于形参是实参的别名,形参的改变会导致实参的改变
- 传递指针,传递时形参将会生成新的指针变量初始化为实参的值,形参指向的地址改变不会影响实参,但是在不改变指向的时候形参指向的值改变会导致实参指向值的改变
函数的值传递
void swap1(int a, int b)
{
int c = a;
a = b;
b = c;
}
函数的引用传递
void swap2(int& a, int& b)
{
int c = a;
a = b;
b = c;
}
函数的指针传递
void swap3(int* a, int* b)
{
int c = *a;
*a = *b;
*b = c;
}
void f7(int* a)
{
int c = 10;
a = &c;
}
测试
void test()
{
int a = 3, b = 5;
cout << "调用值传递函数前" << endl;
cout << "a = " << a << " " << "b = " << b << endl;
swap1(a, b);
cout << "调用值传递函数后" << endl;
cout << "a = " << a << " " << "b = " << b << endl;
cout << endl << endl;
a = 10;
b = 20;
cout << "调用引用传递函数前" << endl;
cout << "a = " << a << " " << "b = " << b << endl;
swap2(a, b);
cout << "调用引用传递函数后" << endl;
cout << "a = " << a << " " << "b = " << b << endl;
cout << endl << endl;
a = 6;
b = 8;
int* pa = &a;
int* pb = &b;
cout << "调用指针传递函数前" << endl;
cout << "a = " << a << " " << "b = " << b << endl;
swap3(pa, pb);
cout << "调用指针传递函数后" << endl;
cout << "a = " << a << " " << "b = " << b << endl;
cout << endl << endl;
a = 9;
cout << "未改变指向时a的值" << *pa << endl;
f7(pa);
cout << "改变指向后a的值" << *pa << endl;
}
结果如下
验证了值传递不改变实参的大小,指针传递和引用传递可以改变值的大小,同时指针传递改变指针的指向不会引起实参的指向
函数的返回值
- 无返回值的函数 :没有返回值的return语句只能用于返回值类型为void的函数,无返回值的函数,最后不一定非要有return语句,函数的最后一句会隐式地执行return
- 有返回值的函数 : return语句的第二种形式提供看函数的结果,只要函数的返回值类型不是void,则该返回值的每一条return语句必须返回一个值,且return语句的返回值类型和函数的返回值相同,或者能隐式转换成函数的返回值类型,
//无返回值的函数
void printArray()
{
for (int i = 0; i < 10; i++)
cout << i << " ";
//return;
}
//有返回值的函数
bool copmare(string& s1, string& s2)
{
if (s1.size() == s2.size())
return s1 == s2;
int sz = s1.size() < s2.size() ? s1.size() : s2.size(); //获取s1和s2中长度较小的字符串长度
for (int i = 0; i < sz; i++)
{
if (s1[i] != s2[i])
{
//return; //非法,没有返回值
return false;
}
}
return true; //for循环后也要有返回值
}
值是如何被返回的
- 按值返回 :返回值是string类型,函数计算出string后生成一个临时的string变量,将结果赋值给临时的stirng变量返回
- 按引用返回 :返回引用则不会生成临时的string变量,直接将s1和s2中长度较短的返回
- 按指针返回 : 返回指针会生成一个临时的指针变量指向当前指针指向的内存空间
//返回值
string getStr(string& s1, string& s2)
{
return s1.size() > s2.size() ? s1 : s1 + s2;
}
//返回引用
string& shortString(string& s1, string& s2)
{
return s1.size() < s2.size() ? s1 : s2;
}
//返回指针
int* getInt(int* a)
{
return a;
}
不要返回临时变量的引用或者指针
//在函数中的变量会在函数调用结束后对变量的内存进行释放,所以如果返回了函数内部的局部变量,最后引用
//或者指针会指向一个无效的内存
int& getInt1()
{
int a = 1;
return a;
}
int* getInt2()
{
int a = 10;
int* p = &a;
return p;
}
测试
void test3()
{
cout << "无返回值的函数" << endl;
printArray();
cout << endl << endl;
string s1 = "hello";
string s2 = "world";
cout << "有返回值的函数" << endl;
cout << copmare(s1, s2) << endl;
cout << endl << endl;
string s3 = "hehe";
string s4 = "hahaha";
cout << "按值返回的函数" << endl;
cout << getStr(s3, s4) << endl;
cout << endl << endl;
string s5 = "你好";
string s6 = "我很好,你呢";
cout << "按引用返回的函数" << endl;
cout << shortString(s5, s6) << endl;
cout << endl << endl;
int a = 1;
int* pa = &a;
cout << "按指针返回的函数" << endl;
cout << *getInt(pa) << endl;
cout << endl << endl;
cout << "返回临时变量的引用" << endl;
cout << getInt1() << endl;
cout << endl << endl;
cout << "返回临时变量的指针" << endl;
cout << *getInt2() << endl;
cout << endl << endl;
}
结果
结果种返回临时变量的指针或者引用也可以得到想要的结果,主要是程序太短,栈的空间还够用,不需要马上释放函数所占用的内存。
类与对象
C++类的定义
类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的技术。类的接口包括用户所能执行的操作;类的实现则包含类的数据成员。负责接口实现的函数以及定义类所需的私有函数。
封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。
类要想实现数据抽象和封装,需要首先定义一个抽象数据类型,在抽象数据类型中,由类的设计者考虑类的实现过程;使用该类的程序员则只需要抽象地思考类型做了什么。而不用了解类型的工作细节。
定义一个类本质是定义一个数据结构的蓝图,这实际上并没有定义任何数据,但它定义了类的名称意味着申明,也就是说,它定义了类的对象包括了什么,以及可以在这个对象上执行那些操作。
//定义抽象数据类型
//类的定义格式:class + 类名 + 类的主体(花括号包含) 类的定义后必须跟着一个分号
class Box
{
//关键字publi确定了类成员的访问属性。在类的作用域内,公共成员在内的外部
//也可以被访问,访问属性还有protected和private
public:
//类的成员数据
double length; //盒子的长度
double breadth; //盒子的宽度
double heigth; //盒子的高度
//类的成员函数
double get(); //计算盒子的体积
void set(double len, double bre, double hei); //设置盒子的长宽高
};
定义C++对象
// 申明了三个类型为Box的变量
Box box1;
Box box2;
Box box3;
访问数据成员
类与对象实例
//定义抽象数据类型
//类的定义格式:class + 类名 + 类的主体(花括号包含) 类的定义后必须跟着一个分号
class Box
{
public:
//类的成员数据
double length; //盒子的长度
double breadth; //盒子的宽度
double heigth; //盒子的高度
//类的成员函数
double get(); //计算盒子的体积
void set(double len, double bre, double hei); //设置盒子的长宽高
};
//成员函数的定义
double Box::get()
{
return length * breadth * heigth;
}
void Box::set(double len, double bre, double hei)
{
length = len;
breadth = bre;
heigth = hei;
}
//关键字public确定了类成员的访问属性。在类对象作用域内,公共成员在类的外部是可
//可访问的。
//定义类对象 类名+变量名
void test()
{
//申明了三个类型为Box的变量
Box box1;
Box box2;
Box box3;
double volume = 0.0; //保存体积的变量
//box1的参数
box1.length = 1.0;
box1.breadth = 2.0;
box1.heigth = 3.0;
//box2的参数
box2.length = 4.0;
box2.breadth = 5.0;
box2.heigth = 6.0;
//使用成员数据设置参数
volume = box1.length * box1.breadth * box2.heigth;
cout << "box1的体积为:" << volume << endl;
volume = box2.length * box2.breadth * box2.heigth;
cout << "box2的体积为:" << volume << endl;
//使用成员函数设置参数并求体积
box3.set(7.0, 8.0, 9.0);
volume = box3.get();
cout << "box3的体积为:" << volume << endl;
}
运行结果
类的成员函数
类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样类成员函数是类的一个成员,它可以操作类的任意对象,可以访问对象中的所有成员。
前面写的box类是将所有的成员变量修饰成public,这样在类的外部也可以直接访问,现在,我们使用类的成员函数对这些变量进行操作,从而隐藏类的成员变量的细节。
直接访问类的成员变量
class Box
{
public:
//类的成员数据
double length; //盒子的长度
double breadth; //盒子的宽度
double heigth; //盒子的高度
//类的成员函数
double get(); //计算盒子的体积
};
通过成员函数访问成员变量
//使用成员函数访问成员变量
class Box
{
public:
//类的成员数据
double length; //盒子的长度
double breadth; //盒子的宽度
double heigth; //盒子的高度
//类的成员函数
double get(); //计算盒子的体积
void setLength(double len); //设置盒子的长
void setBreadth(double bre); //设置盒子的宽
void setHeight(double hei); //设置盒子的高
};
成员函数可以定义在类定义内部,或者单独使用范围解析符::来定义。在类定义中定义的成员函数默认为内联函数,可以不使用inline关键字。
类内部定义
class Box
{
public:
//类的成员数据
double length; //盒子的长度
double breadth; //盒子的宽度
double heigth; //盒子的高度
//类的成员函数
//计算盒子的体积
double get()
{
return length * breadth * heigth;
}
};
类外部定义
//使用范围解析符::在类外定义
//成员函数的定义
double Box::get()
{
return length * breadth * heigth;
}
类的成员函数实例
//使用成员函数访问成员变量
class Box
{
public:
//类的成员数据
double length; //盒子的长度
double breadth; //盒子的宽度
double heigth; //盒子的高度
//类的成员函数
double get(); //计算盒子的体积
void setLength(double len); //设置盒子的长
void setBreadth(double bre); //设置盒子的宽
void setHeight(double hei); //设置盒子的高
};
void Box::setLength(double len)
{
length = len;
}
void Box::setBreadth(double bre)
{
breadth = bre;
}
void Box::setHeight(double hei)
{
heigth = hei;
}
double Box::get()
{
return length * breadth * heigth;
}
void test()
{
Box box1;
Box box2;
//设置box1的参数
box1.setLength(3);
box1.setBreadth(4);
box1.setHeight(5);
//设置box2的参数
box2.setLength(6);
box2.setBreadth(7);
box2.setHeight(8);
cout << "box1的体积为:" << box1.get() << endl;
cout << "box2的体积为: " << box2.get() << endl;
}
运行结果
box1的体积为: 60
box2的体积为: 336
类的访问修饰符
前面说到类的三大特性,封装、继承和多态,封装是将成员数据和成员函数写在类的内部实现的,那么前面我们所写的成员变量可以在类的外部访问,那么这可能造成类成员的一些安全隐患,我们需要将一些不需要被类外访问的数据通过访问修饰符禁止类外访问。
类的访问修饰符有public、protected和private,修饰范围从申明开始到下一个访问修饰符或者类主体结束的括号,成员和类的访问修饰符默认为private。
修饰范围
//修饰范围
class base {
public:
//共有成员
protected:
//保护成员
private:
//私有成员
};
公有成员(public)
//共有成员在程序中类的外部也是可见的,可以直接访问类的成员变量而不用通过类的成员函数
//来设置或者获取它们的值
class Line
{
public:
double length;
void setLength(double len);
double getLength();
};
//成员函数的定义
void Line::setLength(double len)
{
length = len;
}
double Line::getLength()
{
return length;
}
void test2()
{
Line line;
//直接访问成员变量
line.length = 1.0;
cout << "直接访问成员变量 :length = " << line.length << endl;
//使用成员函数访问成员变量
line.setLength(2.0);
cout << "通过成员函数访问成员变量:length = " << line.getLength() << endl;
}
运行结果
直接访问成员变量 :length = 1
通过成员函数访问成员变量:length = 2
私有成员(private)
//私有成员在类的外部不可见,只有类的成员函数或者友元函数可以访问,默认情况下类的成员是私有的
class Rect
{
double length;
public:
double width;
void setLength(double len);
double getLength();
};
void Rect::setLength(double len)
{
length = len;
}
double Rect::getLength()
{
return length;
}
void test3()
{
Rect rect;
//rect.length = 3.0 非法,length在类外不可见
rect.setLength(3.0);
cout << "通过成员函数访问成员变量:length = " << rect.getLength() << endl;
rect.width = 4.0;
cout << "直接访问成员变量 :length = " << rect.width << endl;
}
运行结果
通过成员函数访问成员变量:length = 3
直接访问成员变量 :length = 4
受保护成员(protected)
//保护成员和私有成员很像,但有一点不同,保护成员在派生类中也可以访问
class Circle
{
protected:
double r;
public:
void setr(double _r);
double getr();
};
void Circle::setr(double _r)
{
r = _r;
}
double Circle::getr()
{
return r;
}
void test4()
{
Circle circle;
//circle.r = 5.0 非法,r在类外不可见
circle.setr(5.0);
cout << "通过成员函数访问成员变量:r = " << circle.getr() << endl;
}
运行结果
通过成员函数访问成员变量:r = 5
类的构造函数和析构函数
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行,构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回void,构造函数可用于为某些成员变量设置初始值。
类的析构函数是类的一种特殊的成员函数,它会在每次删除对象时执行,析构函数的名称和类的名称是完全相同的,只是在函数前面加了个波浪线(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序前释放资源。
无参数的构造函数
class Point
{
int x;
int y;
public:
void setX(int _x);
void setY(int _y);
int getX();
int getY();
Point();
};
Point::Point()
{
cout << "无参数构造函数被调用了" << endl;
}
void Point::setX(int _x)
{
x = _x;
}
void Point::setY(int _y)
{
y = _y;
}
int Point::getX()
{
return x;
}
int Point::getY()
{
return y;
}
void test5()
{
Point p1;
//设置点的x,y坐标
p1.setX(1);
p1.setY(2);
//输出点的坐标
cout << "p1 的坐标为(" << p1.getX() << "," << p1.getY() << ")" << endl;
}
运行结果
有参数的构造函数
Point(int _x, int _y);
Point::Point(int _x, int _y)
{
cout << "有参数构造函数被调用了" << endl;
x = _x;
y = _y;
}
Point p2(3, 4);
cout << "p2 的坐标为(" << p2.getX() << "," << p2.getY() << ")" << endl;
运行结果
初始化列表的构造函数
Point(int _x, int _y);
Point::Point(int _x, int _y) :x(_x), y(_y)
{
cout << "初始化列表构造函数调用了" << endl;
}
Point p2(3, 4);
cout << "p2 的坐标为(" << p2.getX() << "," << p2.getY() << ")" << endl;
运行结果
析构函数
~Point();
Point::~Point()
{
cout << x << endl;
cout << "析构函数被调用了" << endl;
}
Point p1;
p1.setX(1);
p1.setY(2);
cout << "p1 的坐标为(" << p1.getX() << "," << p1.getY() << ")" << endl;
Point p2(3, 4);
cout << "p2 的坐标为(" << p2.getX() << "," << p2.getY() << ")" << endl;
运行结果
从上面的运行结果中可以看出,p1先构造但是后析构。
类的拷贝构造函数
C++的拷贝构造函数
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前已经创建的对象来初始化新创建的对象,通常有以下应用场景
- 通过同一个类对象初始化新创建的对象;
- 作为参数传递时,形参就是通过实参进行拷贝构造函数创建的;
- 作为函数返回值,返回对象的副本,使用拷贝构造函数复制原对象;
拷贝构造函数的形式
classname(const classname &obj)
参数传递的是对象的引用,这里不能是值传递,如果传递的是值,按照上面说的拷贝构造函数的应用场景,当按值传递时会调用拷贝构造函数对形参进行初始化,那么会进入无限调用拷贝构造函数的循环中,最终导致栈内存溢出。
当我们没有写拷贝构造函数时,会自动生成一个默认的拷贝构造函数,但是,这里还有一个问题,当类中有指针变量的时候,需要自己实现拷贝构造函数,因为指针变量在默认拷贝构造函数时,只会进行浅拷贝,也就是只会生成一个原指针变量的副本,指向原指针指向的内存,那么当原对象销毁时,其指针变量指向的内存也就被回收了,而通过拷贝构造函数生成的对象,其指针就变成了一个悬空指针了,再去对新对象进行析构回收其内存则是件很危险的事情。
拷贝构造函数实例
class Line
{
public:
int getLength();
Line(int len); //简单构造函数
Line(const Line& obj); //拷贝构造函数
~Line(); //析构函数
private:
int* length;
};
//成员函数定义
Line::Line(int len)
{
cout << "调用构造函数" << endl;
length = new int; //为指针变量分配内存
*length = len; //为指针变量指向的内存赋值
}
Line::Line(const Line& obj)
{
cout << "调用拷贝构造函数" << endl;
length = new int;
*length = *obj.length;
}
Line::~Line()
{
cout << "释放内存" << endl;
delete length;
}
int Line::getLength()
{
return *length;
}
void printLength(Line obj)
{
cout << "line的大小 :" << obj.getLength() << endl;
}
void test2()
{
Line line1(10);
printLength(line1);
cout << endl << endl;
Line line2(line1);
printLength(line2);
cout << endl << endl;
Line line3 = line1;
printLength(line3);
cout << endl << endl;
运行结果
结果分析
Line line1(10); //调用构造函数
printLength(line1); //调用拷贝构造函数将形参初始化,函数调用结束后对形参进行析构回收其内存
cout << endl << endl;
Line line2(line1); //调用拷贝构造函数为line2进行初始化
printLength(line2); //调用拷贝构造函数将形参初始化,函数调用结束后对形参进行析构回收其内存
cout << endl << endl;
Line line3 = line1; //调用拷贝构造函数为line3进行初始化
printLength(line3); //调用拷贝构造函数将形参初始化,函数调用结束后对形参进行析构回收其内存
cout << endl << endl;
//此时整个程序结束,类的三个对象line1,line2,line3开始析构回收内存,先生成的对象后析构
this指针和指向类的指针
this指针
在C++中,每个对象都能通过this指针来访问自己的地址,this指针是所有成员函数的隐含参数,因此,在成员函数内部,它可以用来指向调用对象,友元函数不是类的成员函数,它没有this指针,只有成员函数才有this指针。
指向类对象的指针
指向类对象的指针和结构体指针类似,只不过类中除了变量还有函数,通过申明类的指针然后对指针赋值,通过->符号就可以访问当前指针指向对象的成员。
代码实例
class Line
{
public:
Line(int length) {
cout << "构造函数被调用" << endl;
this->length = length; //成员变量的this指针指向当前类的变量
}
bool compare(Line line)
{
return this->length > line.length; //成员函数内也可以用this指针访问当前类的变量
}
private:
int length;
};
void test2()
{
Line line1(5);
Line line2(8);
Line* ptr1 = &line1; //保存第一个对象的地址
Line* ptr2 = &line2; //保存第二个对象的地址
if (ptr1->compare(*ptr2)) //类的指针访问成员和结构体指针访问成员一样需要用->
{
cout << "line1 is larger than line2" << endl;
}
else
{
cout << "line1 is equal to or smaller than line2" << endl;
}
}
运行结果
类的静态成员
静态成员是用static关键字修饰的成员,分为静态成员变量和静态成员函数,申明为静态成员时,表示无论创建了多少个类的对象,类成员都只有一个副本。
类的成员变量需要在类内申明,但是定义在类外。在内外对静态变量初始化时可以使用范围解析符::来重新申明静态变量并对其进行初始化,如果没有在类外对它进行初始化,那么在第一个对象创建时,就会默认将静态变量初始化为0.
静态成员函数和普通成员函数的区别
- 静态成员函数没有this指针,只能访问静态成员
- 普通成员函数有this指针,可以访问类中的任意成员(包括静态成员)
class Line
{
public:
static int count;
Line(int len) {
cout << "构造函数被调用" << endl;
length = len;
count++;
}
static int getCount()
{
return count;
}
private:
int length;
};
//类外对静态成员变量初始化
int Line::count = 0;
void test2()
{
cout << "未创建对象时对象的个数" << endl;
cout << "Total of objects : " << Line::count << endl;
Line line1(1);
Line line2(2);
cout << "通过静态变量获得对象个数" << endl;
cout << "Total of objects : " << Line::count << endl;
cout << "通过静态成员获得对象的个数" << endl;
cout << "Totla of objects : " << Line::getCount() << endl;
}
运行结果
友元函数和友元类
类的友元函数是定义在类外部,但是有权限访问类的私有和保护成员,但是友元函数不是类的成员函数,其申明在类的外部,只不过在类中申明为它的友元,目的是给它权限对类的成员进行访问,当然这失去了 类的访问控制的意义,因为类的成员对友元函数是全可见。
申明友元类所在的类对友元类的所有成员可见,但是有以下问题需要注意
- A类是B类的友元,B并不是A的友元;
- A类是B类的友元,B类是C类的友元,但是A类并不是C类的友元,友元属性没有传递性
- 友元不能被继承
友元函数
class Line {
public:
friend void printLength(class Line);
Line(int len);
~Line();
private:
int length;
};
Line::Line(int len)
{
cout << "调用构造函数" << endl;
length = len;
}
Line::~Line()
{
cout << "调用析构函数" << endl;
}
void printLength(Line line)
{
cout << "line of length : " << line.length << endl;
}
void test2()
{
Line line(5);
printLength(line);
cout << endl << endl;
}
运行结果
友元类
class A {
public:
A(int _x);
friend class B;
private:
int x;
};
A::A(int _x)
{
x = _x;
}
class B {
public:
B(int _y);
int getA(A a);
private:
int y;
};
B::B(int _y)
{
y = _y;
}
int B::getA(A a)
{
return a.x;
}
void test3()
{
A a(2);
B b(3);
cout << "通过友元类访问申明其友元所在类的私有成员变量" << endl;
cout << "A类的私有成员变量为:" << b.getA(a) << endl;
}
运行结果
函数重载和运算符重载
C++允许在同一个作用域内中的某个函数或者运算符指定多个定义,分别有函数重载和运算符重载,重载就是函数和方法具有相同的名称但是它们的参数列表不同,调用重载函数或者运算符时会根据参数列表进行匹配找到最适合的定义。
函数重载
同名函数参数列表不同:1.参数的个数不同;2.参数的类型不同;
特别注意
:不能根据函数的返回值不同来区分不同的重载函数
class PrintData {
public:
void print(int i)
{
cout << "整型数为 :" << i << endl;
}
void print(double f)
{
cout << "浮点数为 :" << f << endl;
}
void print(string str)
{
cout << "字符串为 :" << str << endl;
}
};
void test1()
{
cout << "函数重载" << endl;
PrintData obj;
//输出整型数
obj.print(6);
//输出浮点数
obj.print(7.8);
//输出字符串
obj.print("hello world");
cout << endl << endl;
}
运行结果
运算符重载
C++内置的运算符只支持对基本的数据类型进行运算,当我们用到自定义的数据类型时,可以根据需要将运算符进行重载,使其能够进行自定义类型的运算,运算符重载本质上是运算符为名称的函数,其格式为 返回值 + operator待重载的运算符 + 参数列表,和函数重载的形式差不多,只是多了opreator这个关键字。
class Line
{
int length;
public:
Line(int len = 0)
{
length = len;
}
//单目运算符
//重载取反运算符
Line operator-()
{
Line line;
line.length = -this->length;
return line;
}
//++i
Line operator++()
{
this->length = ++this->length;
return *this;
}
//i++
Line operator++(int)
{
Line obj = *this;
++this->length;
return obj;
}
//重载+运算符
Line operator+(Line obj)
{
Line line;
line.length = this->length + obj.length;
return line;
}
//重载-运算符
Line operator-(Line obj)
{
Line line;
line.length = this->length - obj.length;
return line;
}
//重载*运算符
Line operator*(Line obj)
{
Line line;
line.length = this->length * obj.length;
return line;
}
//重载/运算符
Line operator/(Line obj)
{
Line line;
line.length = this->length / obj.length;
return line;
}
//重载关系运算法
//<,>,==
bool operator < (const Line& line)
{
if (this->length < line.length)
return true;
else
return false;
}
bool operator > (const Line& line)
{
if (this->length > line.length)
return true;
else
return false;
}
bool operator == (const Line& line)
{
if (this->length == line.length)
return true;
else
return false;
}
//重载输入输出运算符
friend ostream& operator<<(ostream& output, const Line& line)
{
output << "line of length : " << line.length << endl;
return output;
}
friend istream& operator>>(istream& input, Line& line)
{
input >> line.length;
return input;
}
//重载函数调用运算符
Line operator()(int a, int b)
{
Line line;
line.length = a + b;
return line;
}
int getLength()
{
return length;
}
};
单目运算符
Line operator-()
{
Line line;
line.length = -this->length;
return line;
}
//++i
Line operator++()
{
this->length = ++this->length;
return *this;
}
//i++
Line operator++(int)
{
Line obj = *this;
++this->length;
return obj;
}
void test2()
{
//单目运算符
cout << "单目运算符重载" << endl;
Line line(6);
Line line1 = -line;
cout << "取反后的结果 : " << line1.getLength() << endl;
cout << endl << endl;
cout << "前缀自增和后缀自增" << endl;
Line line2(10), line3, line4;
cout << "未自增前 : " << endl;
cout << "line2 : " << line2.getLength() << endl;
cout << "line3 : " << line3.getLength() << endl;
cout << "line4 : " << line4.getLength() << endl;
line3 = line2++;
cout << "后缀自增后 : " << endl;
cout << "line2 : " << line2.getLength() << endl;
cout << "line3 : " << line3.getLength() << endl;
line4 = ++line2;
cout << "前缀自增后 : " << endl;
cout << "line2 : " << line2.getLength() << endl;
cout << "line4 : " << line4.getLength() << endl;
}
运行结果
双目运算符
//重载+运算符
Line operator+(Line obj)
{
Line line;
line.length = this->length + obj.length;
return line;
}
//重载-运算符
Line operator-(Line obj)
{
Line line;
line.length = this->length - obj.length;
return line;
}
//重载*运算符
Line operator*(Line obj)
{
Line line;
line.length = this->length * obj.length;
return line;
}
//重载/运算符
Line operator/(Line obj)
{
Line line;
line.length = this->length / obj.length;
return line;
}
void test3()
{
Line line1(6);
Line line2(2);
Line line3 = line1 + line2;
cout << "重载+运算符号" << endl;
cout << "line3 :" << line3.getLength() << endl;
line3 = line1 - line2;
cout << "重载-运算符号" << endl;
cout << "line3 :" << line3.getLength() << endl;
line3 = line1 * line2;
cout << "重载*运算符号" << endl;
cout << "line3 :" << line3.getLength() << endl;
line3 = line1 / line2;
cout << "重载/运算符号" << endl;
cout << "line3 :" << line3.getLength() << endl;
}
运行结果
逻辑运算符
//重载关系运算法
//<,>,==
bool operator < (const Line& line)
{
if (this->length < line.length)
return true;
else
return false;
}
bool operator > (const Line& line)
{
if (this->length > line.length)
return true;
else
return false;
}
bool operator == (const Line& line)
{
if (this->length == line.length)
return true;
else
return false;
}
void test4()
{
Line line1(6);
Line line2(5);
if (line1 < line2)
cout << "line1 is samller than line2" << endl;
else
cout << "line1 is not samller than line2" << endl;
if(line1 > line2)
cout << "line1 is larger than line2" << endl;
else
cout << "line1 is not larger than line2" << endl;
if(line1 == line2)
cout << "line1 is equal to line2" << endl;
else
cout << "line1 is not equal to line2" << endl;
}
运行结果
输入输出运算符
//重载输入输出运算符
friend ostream& operator<<(ostream& output, const Line& line)
{
output << "line of length : " << line.length << endl;
return output;
}
friend istream& operator>>(istream& input, Line& line)
{
input >> line.length;
return input;
}
void test5()
{
Line line;
cin >> line;
cout << line;
}
函数()运算符
// 重载函数调用运算符
Line operator()(int a, int b)
{
Line line;
line.length = a + b;
return line;
}
void test6()
{
Line line1(6), line2;
line2 = line1(2, 3);
cout << line1 << endl;
cout << line2 << endl;
}
运行结果
索引运算符
const int SIZE = 10;
class MyArray {
int arr[SIZE];
public:
MyArray()
{
for (int i = 0; i < SIZE; i++)
{
arr[i] = i;
}
}
//重载[]
int& operator[](int index)
{
if (index >= SIZE)
{
cout << "索引超过最大值 ";
return arr[0];
}
return arr[index];
}
};
void test7()
{
MyArray arr;
cout << "arr[1]的值为 : " << arr[1] << endl;
cout << "arr[6]的值为 : " << arr[6] << endl;
cout << "arr[12]的值为 : " << arr[12] << endl;
}
运行结果
继承
前面说了面向对象的三个特性之一封装,将实现细节封装到类结构中,只对外提供访问类成员的接口,让程序员更加关注功能而不去在意实现细节,提高编程效率的同时也保证了数据的安全性,今天说它的另外一个特性继承,当我们写一个类时,不需要重新写新的成员变量和成员函数时,只需要指向新建的类继承一个已经有的类即可,这个被继承的类称为基类,新建的类称为派生类。
//基类
class Animal
{
//eat() 函数
//sleep() 函数
};
//派生类
class Dog : public Animal
{
//bark() 函数
};
一个类可以派生自多个类,也就是说它可以从多个基类继承数据和函数。定义一个派生类,我们可以使用类派生列表来指定基类,类派生列表由一个或者多个基类命名形式为:class 派生类名 : 继承方式 基类名。
访问控制和继承
派生类可以访问基类中所有的非private成员,如果基类的成员不想被派生类访问则应该将其申明为private,派生类继承了基类所有的方法,但以下情况除外:
- 基类构造和析构函数
- 基类友元函数
- 基类的重载运算符
继承类型
- 公有继承:基类的public和protected成员是派生类的publich和protected成员,可以被派生类访问,基类的private成员不能被派生类访问;
- 保护继承:基类的public和protected成员是派生类的protected成员,可以被派生类访问,基类的private成员不能被派生类访问;
- 私有继承:基类的public和protected是派生类的private成员,派生类不能访问,基类的private成员不能被派生类访问;
多继承
一个派生类有多个基类,继承了多个基类的特性class 派生类名:继承方式1 基类名1, 继承方式2 基类名2…
//基类
class Shape {
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
heigth = h;
}
protected:
int width;
int heigth;
};
class PaintCost {
public:
int getCost(int area)
{
return 6 * area;
}
};
//派生类
class Rectangle : public Shape, public PaintCost{
public:
int getArea()
{
return width * heigth;
}
};
void test2()
{
Rectangle rect;
rect.setHeight(3);
rect.setWidth(4);
int area = rect.getArea();
cout << "area of rect is : " << area << endl;
cout << "cost of paint is : " << rect.getCost(area) << endl;
}
运行结果
多态
多态指的是同一个方法,可以有不同的响应消息,分为编译期多态和运行时多态,编译期多态是通过函数重载实现的,在程序编译期就确定了具体响应哪个函数,运行时多态是通过虚函数实现的,在程序运行时才能确定具体响应哪个函数。
前面已经说过了函数重载,现在我们在来看看虚函数是如何实现运行时多态,当两个类存在继承关系,且基类存在虚函数,那么在子类中重写了这个继承的虚函数,再通过基类的指针指向派生类的对象时,就可以实现运行时多态。
class Shape
{
protected:
int heigth;
int width;
public:
Shape(int h = 0, int w = 0) {
heigth = h;
width = w;
}
virtual int getArea()
{
cout << "shape class area :" << endl;
return 0;
}
};
class Rectangle : public Shape {
public:
Rectangle(int h, int w):Shape(h, w){}
int getArea()
{
cout << "rectangel class area :" << endl;
return heigth * width;
}
};
class Triangle :public Shape {
public:
Triangle(int h, int w) :Shape(h, w) {}
int getArea()
{
cout << "triangle class area : " << endl;
return heigth * width / 2;
}
};
void test()
{
Shape *shape = new Shape;
cout << shape->getArea() << endl;
Rectangle rect(6, 8);
Triangle tria(4, 6);
shape = ▭
cout << shape->getArea() << endl;
shape = &tria;
cout << shape->getArea() << endl;
}
运行结果
纯虚函数
纯虚函数(接口,抽象类)
接口描述了类的行为和功能,而不需要完成类的特定实现,C++接口是使用抽象类实现的,含有纯虚函数的类为抽象类,纯虚函数的形式为:virtual + 返回值类型 + 函数名 + 参数列表 = 0,抽象类不能实例化,要想实例化必须在其子类实现所有的纯虚函数。
class Shape
{
protected:
int heigth;
int width;
public:
Shape(int h = 0, int w = 0) {
heigth = h;
width = w;
}
virtual int getArea() = 0;
};
class Rectangle : public Shape {
public:
Rectangle(int h, int w):Shape(h, w){}
int getArea()
{
cout << "rectangel class area :" << endl;
return heigth * width;
}
};
class Triangle :public Shape {
public:
Triangle(int h, int w) :Shape(h, w) {}
int getArea()
{
cout << "triangle class area : " << endl;
return heigth * width / 2;
}
};
void test()
{
//Shape shape; 非法,Shape是抽象类,不能实例化
Rectangle rect(6, 8);
Triangle tria(4, 6);
cout << rect.getArea() << endl;
cout << tria.getArea() << endl;
}
运行结果
rectangel class area :
48
triangle class area :
12