c++学习课件(一)
day01
一、C++ 介绍
1. 语言的产生
C++ 由 Bjarne Stroustrup 于 1979 年在贝尔实验室开始设计开发的,由于C++ 进一步扩充和完善了 C 语言(面向过程),是一种面向对象的程序设计语言 ,所以最初命名为带类的C 。C++ 是 C 的一个超集,事实上,任何合法的 C 程序都是合法的 C++ 程序。C++ 也是目前嵌入式 ( 无人驾驶 ) 的首选语言。 扫地机器人 (激光雷达 、 车轮子 )… 。游戏引擎(底层代码 , c++ )
至少掌握一门编程语言 python , java , c/c++
[外链图片转存失败,
2. C++发展
C++语言发展大概可以分为三个阶段:
- 第一阶段 从80年代到1995年。这一阶段C++语言基本上是传统类型上的面向对象语言,并且凭借着接近C语言的效率,在工业界使用的开发语言中占据了相当大份额;
- 第二阶段 从1995年到2000年,这一阶段由于标准模板库(STL)和后来的Boost等程序库的出现,泛型程序设计在C++中占据了越来越多的比重性。当然,同时由于Java、C#等语言的出现和硬件价格的大规模下降,C++受到了一定的冲击;
- 第三阶段 从2000年至今,由于以Loki、MPL等程序库为代表的产生式编程和模板元编程的出现,C++出现了发展历史上又一个新的高峰,这些新技术的出现以及和原有技术的融合,使C++已经成为当今主流程序设计语言中最复杂的一员。
3. C++优缺点
- 优点
- 实现了面向对象程序设计。在高级语言(底层 , )当中,处理运行速度是最快的,大部分的游戏软件,系统都是由C++来编写的
- 功能强大,嵌入式首选。
- C++加上严格的codereview (代码检查)可以用来开除猪一样的队友,这是所有其他语言都不具备的功能
- 缺点
- 学习门槛高、难学
- 知识点繁杂,有声音说,c++标准委员会仅仅是为炫技并不考虑实际的工业需求
4. 与python比较
程序有两种执行方式,解释执行和编译执行。
python是一种解释语言(弱类型语言 | 动态类型语言),不需要经过编译,是一边解释一边执行,由于底层封装了很多库供程序员使用,所以开发起来方便快捷,且能够很好地跨平台,写一些小工具小程序特别合适。python —> exe —> 发布源码c++ 则属于编译语言 , (静态类型语言 | 强类型语言),是一种需要编译后运行语言,编译后通过链接,整合其他依赖库,然后生成机器码(可执行文件),以后再运行,无需编译。在指定环境下编译运行,运行效率高 ,c++ —> exe -> 很难查看源码
- python 代码打印
python xx.py
print(66)
print(66)
print(66)
print(66)
print(66)
print(66)
print(66)
- c++ 代码打印
#include <iostream>
int main(){
int x = 5;
std::cout << x << std::endl;
return 0 ;
}
二、环境准备
编译环境 + 开发工具
python : python.exe + pycharm
c++ : mingw + clion
1. 安装编译环境
a. GCC 系列
GNU编译器套装(英语:GNU Compiler Collection,缩写为GCC),指一套编程语言编译器,常被认为是跨平台编译器的事实标准。原名是:GNU C语言编译器(GNU C Compiler) 起初只能编译C语言, 之后也变得可处理
c++
、Fortran
、Objective-C
、Ada
,Go
与其他语言。另外许多类Unix系统,把
gcc
当成是标准编译器,比如:Linux
Unix —> linux | mac os
windows : ms-dos
- MinGW
mingw 名称 全称 W windows.
又称mingw32 , 是将GCC编译器和GNU Binutils移植到Win32平台下的产物。MinGW占用内存、硬盘空间都比较少,能够链接到任意软件,但它对 POSIX 规范的实现没有 Cygwin 库完备。
- Cygwin
Cygwin的主要目的是通过重新编译,将POSIX系统(例如Linux、BSD,以及其他Unix系统)上的软件移植到Windows上 。 Cygwin包括了一套库,该库在Win32系统下实现了POSIX系统调用的API;还有一套GNU开发工具集(比如GCC、GDB)
b. MSVC系列
与Visual Studio集成发布,微软自己的编译器,VS是一个基本完整的开发工具集,它包括了整个软件生命周期中所需要的大部分工具,如UML工具、代码管控工具、集成开发环境(IDE)等等。
2. 安装开发工具
开发工具种类繁多,有
VS
|Dev-C++
|code lite
|clion
|Qt Creator
三、HelloWorld
1. 入门示例
- 示例代码如下:
#include<iostream>
int main (){
std::cout << "hello world !" << std::endl;
return 0 ;
}
理解:
#include
: 引入输入输出库
int main(){}
: 主函数入口
std::
… 输出打印
return 0
: 函数返回值,一般0 表示正常执行返回。
2. main函数解释
main
函数是C++程序的__入口函数__,C++标准要求main
函数的返回值类型为int
。 0 表示正常退出。
int main() {
return 0;
}
- 返回值释疑
当
main
函数的返回值为int
,而函数内并没有出现return语句时,同样可以通过编译并正常运行。这是因为编译器在
main
函数的末尾__自动添加了return 0;
的语句__。所以,
main
函数是C++程序经过__特殊处理的函数__。其他的返回值类型不是
void
的函数,如果没有使用return语句,编译器将报错。
int main(){
}
- 参数释疑
main函数有两种写法: 一种是不含参数,一种是携带参数。当考虑程序在运行的时候,立即获取到外部传递进来的数据,那么可以使用含有参数的main函数。 参数
argc
表示传递进来的参数总数,args
表示传递进来的参数。char **
是二级指针的写法,当然此时我们是不明白二级指针的意思。那么把它看成是二维数组就好了。 值得注意的是: main函数的第一个参数会隐式传递进来当前程序的全路径地址。
int main(int argc , char ** args) {
std::cout << "参数个数:"<< argc << std::endl;
std::cout << "第一个参数:"<< args[0] << std::endl;
std::cout << "第一个参数:"<< args[1] << std::endl;
return 0;
}
3. 命令函编译
实际上除了使用IDE工具编写C++代码之外,也可以使用记事本编写,然后使用命令行的方式编译执行写好的C++代码。
#include <iostream>
int main() {
std::cout << "Hello world!" << std::endl;
return 0;
}
- 打开命令行,输入以下命令即可
g++ -Wall -std=c++11 main.cpp
g++ -Wall -std=c++14 main.cpp -o main.exe -W all
g++
: 是编译工具.
-Wall
: 编译过程中显示所有的警告.
-std=c++11
: 使用c++11版本进行编译.
main.cpp
: 要编译的cpp文件
编译执行完成后,Windows下默认生成 a.exe 文件,linux下默认生成 a.o 文件,macos下默认生成 a.out 文件
-o
: 指定输出文件名称
4. 注释
每一种语言都有注释,以便程序员对其编写的代码进行解释说明。c++采用
//
和/* */
实现单行和多行注释。
#include <iostream>
int main (){
// 我是c++的单行注释
std::cout << "hello world" << std::endl;
/*
我是c++的多行注释
我可以写多行
*/
std::cout << "hello world" << std::endl;
return 0 ;
}
练习
main函数练习
四、 数据类型
与其他静态类型语言一样,C++也具备大多数语言的数据类型。除下表给出的数据类型之外,其实还有
short
|long
|long long
等 。 关于数据类型占用的字节长度其实不用刻意的去背,c++提供sizeof()函数
来获取某种数据类型占用的空间大小。
数据类型 | 声明 | |
---|---|---|
整数类型 | int | 10 |
单精度浮点数 | float | 10.5 空间更小 |
双精度浮点数 | double | 10.5 能显示更多的小数位 ,空间更大 |
字符 | char | ‘a’ |
字符串 | string | “aabbcc” |
布尔 | bool | true | false |
五、变量
1. 声明和初始化
c++ 是强类型语言,任何变量在使用前必须先声明,并且在声明的时候必须要指定该变量的数据类型,即该变量未来能够存储的数据类型。
int main(){
//先声明
int age ;
double price;
//后初始化
age = 10 ;
price = 20.3;
//声明及初始化
int age = 20 ;
return 0 ;
}
- 变量声明以及初始化常用手法:
int age ; //未初始化
int age = 21 ; // C 方式初始化
int age (21); //构造方法初始化
int age {21} ; //c++ 11标准开始的方式
2. 变量命名规则
- 可以包含字母 、 数字 和下划线
- 必须以字母或者下划线开始
注意:在C++中,大写字母和小写字母被认为是两个不同的字符。因此,sum和SUM是两个不同的变量名。一般地,变量名用小写字母表示,与人们日常习惯一致,以增加可读性。应注意变量名不能与C++的关键字、系统函数名和类名相同
有效命名 | 无效命名 |
---|---|
Age | 2020_Age |
age | Age+1 |
_age | $age |
My_Age | My Age |
Int | return |
3. 常量
常量其实和变量没有多大区别, 有名字, 占据存储空间,可以是任何的基本类型,但只有一点不同,常量的值不允许变更。C++中的常量的声明需要使用
const
关键字,而python中的常量默认约定是全大写表示。
int main(){
const double pi {3.1415926};
const int months_in_year{12};
pi = 2.5 ; //将会出现编译错误
return 0 ;
}
4. 变量长度
sizeof
除了可以作用于类型身上之外,也可以作用于变量身上。
#include<iostream>
int main(){
double a = 20.0;
sizeof(a); //8
sizeof(int); //4
return 0 ;
}
六、常见编码错误
- 忘记分号结尾
int main (){
int x //此处编译错误
x = 5;
return 0;
}
- 声明和定义错误
#include <iostream>
int main (){
x = 5; //没有声明
std::cout << x;
return 0;
}
- 命名空间错误
#include <iostream>
int main (){
int x;
x = 5;
cout << x; //缺少命名空间
return 0;
}
- 库包含错误
int main (){
int x;
x = 5;
std::cout << x; //没有引入 io流 库
return 0;
}
注意: 除了变量 | 对象的声明初始化之外,其他的逻辑代码(需要经过调用 、 运算)都需要在函数内部编写。
七、输入和输出
任何编程语言都要输入和输出,python的输入输出是
input
和scanf
和printf
, 而C++的相对要复杂些,它使用std::cin
和std::cout
来操作输入输出 。 C++的输入输出,需要导入 iostream 库 。
1. 输出
一般在输出的后面跟上
std::endl
来表示输出结束,它除了含有换行的功能之外,还具备了刷新数据打印缓冲区的功能。
#include <iostream>
int main (){
//由于没有换行,两个单词会出于同一行。
std::cout << "Hello";
std::cout << " world";
//兼备换行的输出
std::cout << "Hello" << std::endl;
std::cout << " world" << std::endl;
//可以连续输出打印,这其实是一种叫做:链式调用的手法
std::cout << "Hello " << " World" << " , I love C++!"<< std::endl;
return 0 ;
}
2. 输入
输入旨在获取键盘的数据输入。 不过获取输入前的提示语句得使用
std:: cout
来输出提示。
#include <iostream>
int main (){
std::cout << "请输入您的年龄:"<<std::endl;
int age ;
std::cin >> age;
std::cout << "您的年龄是:" << age <<std::endl;
return 0 ;
}
3. 练习
输入三条的边长,判断能否组成三角形。
三条边长能否组成三角形的 依据 ??
八、条件与循环
1. 条件判断
1. if语句
条件判断即是生活中的对某个事物进行判断,比如:父子间对此次考试成绩进行约定,如果超过90分,则可以自由活动一天,如果超过80分,可以休息半天,如果低于80分,则要乖乖去 写作业。
#include <iostream>
int main (){
std::cout << "请输入此次考试的成绩:" << std::endl;
int score ;
std::cin >> score;
if( score > 90){
std::cout << "可以休息一天" << std::endl;
}else if(score > 80){
std::cout << "可以休息半天" << std::endl;
}else{
std::cout << "乖乖去写作业" << std::endl;
}
return 0 ;
}
2. switch语句
许多语言中都包含switch,比如:java 、javascript 、php 等,而python是个例外,python没有switch。,实际上
switch
和if
语句块很相似, 它是完全的等价条件判断,但是一旦满足某个case
的条件,那么其他case
就不会再进行判断。
#include <iostream>
int main (){
std::cout << "请输入此次考试的成绩评级" << std::endl;
char level ;
std::cin >> level;
switch (level){
case 'A':
std::cout << "优秀" << std::endl;
break;
case 'B':
std::cout << "良好" << std::endl;
break;
case 'C':
std::cout << "及格" << std::endl;
break;
case 'D':
std::cout << "仍需继续努力" << std::endl;
break;
default:
std::cout << "输入错误。" << std::endl;
break;
}
return 0 ;
}
2. 运算符操作
1. 关系运算符
表示大小、相等操作的运算符,python和c++表示方法是一样的。
Operator | Python | C++ |
---|---|---|
equal | == | == |
not equal | != | != |
greater than | > | > |
less than | < | < |
greater than or equal | >= | >= |
less than or equal | <= | <= |
2. 逻辑运算符
有时候需要对多个条件进行一起判断,比如:前面的考试约定,不仅要考察语文的成绩还要考察数学的成绩,不仅仅是单科成绩了。
Operator | Python | C++ |
---|---|---|
并且 | and | && |
或者 | or | || |
非(取反) | not | ! |
#include <iostream>
int main (){
int chinese_score ;
int math_score ;
std::cout << "请输入此次语文考试的成绩:" << std::endl;
std::cin >> chinese_socre;
std::cout << "请输入此次数学考试的成绩:" << std::endl;
std::cin >> math_score;
if( chinese_socre > 90 && math_score > 90){
std::cout << "可以休息一天" << std::endl;
}else if(score > 80 && math_score > 80){
std::cout << "可以休息半天" << std::endl;
}else{
std::cout << "乖乖去写作业" << std::endl;
}
return 0 ;
}
3. 三元运算符
一般来说,非真即假的情况下,如果表达式比较简单,通常会采用三元运算符来实现。在相比之下,c++的三元运算符比python的要简单些。比如下面的例子:如果考试成绩大于90,评为A ,否则评为B,此时针对一个条件的判断,只有两种结果,那么使用三元表达式在简洁程度上要胜过
if
语句。
#include <iostream>
int main (){
int score;
std::cout << "请输入此次语文考试的成绩:" << std::endl;
std::cin >> score;
/*
//python的三元表达式:
result = 'A' if 100 > 90 else 'B'
print(result)
*/
//c++
char result = score > 90 ? 'A' : 'B';
std::cout << "您的语文成绩评级为:" << result <<std::endl;
return 0 ;
}
3. 循环控制
如果需要让某件事重复执行多次,那么循环操作再合适不过了。在c++里面,循环操作有:
while
|do-while
|for
三种实现方式 。 比如模拟路口信号灯闪烁场景
1. while
#include <iostream>
int main (){
int count = 0 ;
while(count < 10){
std::cout << "红灯还在亮着..." << std::endl;
//单位是毫秒
Sleep(1000);
cout++;
}
return 0 ;
}
2. continue 和 break
有时候我们需要对循环的每一次操作,都做一次过滤检查,满足条件的让循环跳过当前,进行下一次循环,或者直接退出循环。在大部分编程语言里面, continue和 break正扮演着这样的角色。比如下面打印1 到 20 的偶数。 如果打印到了16,则直接退出整个循环。
#include <iostream>
int main (){
int number = 1 ;
while(number <= 20){
if(number == 16){
break;
}
//满足条件,表示当前的number是奇数。
if(number % 2 != 0 ){
continue;
}
std::cout << "当前打印的数字是: " << number << std::endl;
number++;
}
std::cout << "循环已结束! "<< std::endl;
return 0 ;
}
3. do-while
do-while结构与while差不多,区别只在于前者是上来先执行操作后判断,后者是先判断再执行循环操作
#include <iostream>
int main (){
int count = 0 ;
do{
std::cout << "红灯还在亮着..." << std::endl;
//单位是毫秒
Sleep(1000);
cout++;
}while(cout < 10);
return 0 ;
}
4. for
在众多循环语句中,
for
循环是使用频率最高的一种。
#include <iostream>
int main (){
for(int cout = 0 ; cout < 10 ; cout ++){
std::cout << "红灯还在亮着..." << std::endl;
//单位是毫秒
Sleep(1000);
}
return 0 ;
}
4. 练习
编程计算图形的面积。程序可计算圆形、矩形的面积,根据用户选择的图形类型来计算。,然后根据不同图形输入不同参数的值,计算出面积的值后输出显示。
如:
请输入您要计算何种类型图形(1: 圆形,2:矩形)
1
请输入圆的半径
2
您要计算的圆的面积为: 12.56
请输入您要计算何种类型图形(1: 圆形,2:矩形)
2
请输入矩形的长度
10
请输入举行的宽度
10
您要计算的矩形的面积是: xxxx
要求使用 : switch (简单版本)
高级版本:
switch 和 循环(高级版本)
循环提问用户,要计算什么类型的图形面积。
while(){
请输入您要计算何种类型图形(1: 圆形,2:矩形 , 0:退出)
}
九、命名空间
假设这样一种情况,当一个班上有两个名叫 张三的学生时,为了明确区分它们,我们在使用名字之外,不得不使用一些额外的信息,比如他们的家庭住址,或者某些特征等等。
同样的情况也出现在 C++ 应用程序中。例如,您可能会写一个名为 a 的变量,在另一个可用的库中也存在一个相同的变量 a。这样,编译器就无法判断您所使用的是哪一个。
因此,引入了命名空间这个概念,专门用于解决上面的问题,它可作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。
1. 自定义命名空间
使用命名空间范围内的成员(变量、函数、类),需要使用 域操作符
::
#include <iostream>
//深圳的张三
namespace shenzhen{
string name = "张三";
}
//武汉的张三
namespace wuhan{
string name = "张三";
}
int main() {
std::cout << shenzhen::name << std::endl;
return 0;
}
2. 使用using指令
可以使用 using namespace 指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。
#include <iostream>
using namespace std;
int main(){
count<<"hi c++" << endl;
return 0 ;
}
练习
上海和香港都有迪士尼,使用命名空间的方式描述香港和上海两地迪士尼的特征(地址、价格、开营时间…) 。请通过获取用户输入的方式,把对应迪士尼的的信息打印出来。
十、数组
c++
的数组实际上和python
的list
差不多,都是具有下标(索引) , 稍有不同的是,python
的list
不区分类型,而c++
的数组必须是同一类型。
- python的数组
scores = [100,98,88,'zhangsan',True]
for s in scores:
print(s)
- c++的数组
#include <iostream>
int main() {
int scores[]{100,95,90,88}
for(int s : scores){
std::cout << s << std::endl;
}
return 0 ;
}
1. 声明和初始化
数组是一系列相同类型的元素,放置在连续的内存位置,数组中的元素都可以通过索引来单独操作它们。 若查看某个变量存储地址可以使用 取地址符
&
- 声明
仅仅声明,而没有初始化的数组,内部的元素无法得到保证,系统会随机进行赋值。
#include <iostream>
int main (){
int scores[5];
//这里遍历打印出来,数组的元素是随机的。
for(int s : scores){
std::cout << "s=" <<s << std::endl;
}
return 0 ;
}
- 初始化
int main(){
//数组类型 数组名称 [元素个数]{初始化列表}
//1. 声明后再初始化
int scores [5];
scores[0] = 11;
scores[1] = 22;
scores[2] = 33;
scores[3] = 44;
scores[4] = 55;
//2. 声明并初始化
int scores [5]{100,89,95,70,80};
int socres [10]{88,75}; //剩下的都会以0占位,只初始化了前两位
int socres [10]{0}; // 表示10个长度的数组,每个元素都是0
//3. 自动推算数组大小
int socres[]{22,33,44,55,66}; //数组长度没有指定,根据后面初始化长度来推断。
return 0 ;
}
2. 访问数组
- 获取数组中的某个元素
数组是具有下标(索引)的容器,可以使用下标 (索引)来获取 , 下标(索引)从
0
开始。型如:数组名称[元素索引]
#include <iostream>
int main(){
//声明并初始化数组
int scores [5]{100,89,95,70,80};
std::cout<<"数组的第一个元素是: "<< scores[0]<<std::endl;
std::cout<<"数组的第二个元素是: "<< scores[1]<<std::endl;
std::cout<<"数组的第三个元素是: "<< scores[2]<<std::endl;
std::cout<<"数组的第四个元素是: "<< scores[3]<<std::endl;
std::cout<<"数组的第五个元素是: "<< scores[4]<<std::endl;
//越界,不会报错,但是输出内容不是我们想看到的
std::cout<<"数组的第一个元素是: "<< scores[5]<<std::endl;
//修改指定位置的元素
scores[0] = 66;
return 0 ;
}
- 遍历数组
c++的数组,并没有提供获取长度的方法,所以不能直接遍历。一种是直接指定遍历的长度,一种是通过代码计算出数组的长度 ,一种是使用c++11提供的基于范围的for循环
#include <iostream>
int main(){
//定义数组
int scores[]{100,95,97,88,85,80,75};
//直接指定数组
for(int i = 0; i < 7; i++){
std::cout << scores[i] << std::endl;
}
//手动计算数组长度
int length = sizeof(scores) / sizeof(int);
for(int i = 0 ; i < length; i++){
std::cout << scores[i] << std::endl;
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++
//c++11 提供的for循环
for(int score : scores){
std::cout <<cores[i] << std::endl;
}
return 0 ;
}
3. 多维数组
数组里面的元素存储的还是数组,即可称之为多维数组。二维数组是常见的多维数组,再多维度的数组阅读起来就稍微有点复杂了。二维数组的操作实际上实际上和一维数组并没有多大区别。
- 声明
数组类型 数组名称[x] [y]; x可以认为是有多少行 , y可以认为是有多少列。多维数组无法进行长度推断,所以具体到行列的长度
4. 练习
使用数组保存张三 , 李四, 王五6个学科成绩,并且计算每个人的总成绩、平均分。成绩从键盘录入。
要求使用二维数组。
十一、打卡作业
1. 使用命名空间定义三个学生,包含变量:姓名、学号、表示6个学科成绩的数组
如果习惯用数组,那么6个学科的成绩就用数组类装,如果不习惯用数组,那么就声明6个变量来接收这个学生的6个学科的成绩。
2. 循环获取三个学生的以上信息。(注意:每个学生有6科成绩要记录)
3. 输入完成后,展示每个学生的总分、平均分。
day02/03
一、 字符串
字符串是最常用的一种数据类型了,在python中声明字符串和声明其他类型的数据一样,都非常的简单。但是在c++中,对于字符串的操作,相对来说要稍微复杂一些。
C++ 提供了以下两种类型的字符串表示形式:
- C 风格字符串
- C++ 引入的 string 类类型
1. C 风格字符串
C 风格的字符串起源于 C 语言,并在 C++ 中继续得到支持。字符串实际上是使用 null 字符 \0 终止的一维字符数组。如下面的声明和初始化创建了一个 “Hello” 字符串。由于在数组的末尾存储了空字符,所以字符数组的大小比单词 “Hello” 的字符数多一个。
int main(){
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
//可以简写成:
char greeting2[] = "Hello";
return 0 ;
}
2. C风格字符串操作
1. 遍历字符串
字符串实际上背后还是一个数组,所以可以使用数组遍历的手法来获取每一个字符 。
#include<iostream>
int main(){
//最后总会跟着一个\0的空字符,此时括号中如果写长度,必须大于等于6
char name[] = "hello";
for (int i = 0; i < sizeof(name ) / sizeof(char); ++i) {
std::cout << name[i] << std::endl;
}
}
2. 字符串其他操作
C语言中提供了针对字符串操作的大量函数,不过在使用之前,需要先引入
#include<cstring>
以下函数的使用,需要引入
#include<ctype.h>
头文件
#include <iostream>
int main(){
//拷贝字符串
char name[] = "hello";
char name2[6];
//参数一: 目标字符串, 参数二:源字符串
strcpy(name2 , name);
std::cout << name2 << std::endl;
//拼接字符串
strcat(name2 , " , 张三");
std::cout << name2 << std::endl;
// 返回字符串长度
int len = strlen(name2);
std::cout << "name2的长度:" << len << std::endl;
return 0 ;
}
2. C++ 风格字符串
C++ 标准库提供了 string 类类型,支持上述所有的操作,另外还增加了其他更多的功能。需要引入
#include<string>
,由于string类声明在命名空间 std ,所以在使用的首要注意 命名空间的联合使用 。
//引入string库
#include <string>
using namespace std;
int mian(){
string s1;
string s2 {"北京"};
string s3{s2};
string s4 = "你好";
s1 = s3;
return 0 ;
}
4. C++风格字符串操作
- 拼接字符串
c++的字符串拼接,使用起来比较方便,直接使用
+
操作即可。
#include<string>
using namespace std;
int main(){
string part1 {"c++"};
string part2 {" is a powerful"};
string sentence ;
sentence = part1 + part2 ;
return 0 ;
}
- 获取指定位置的字符
可以使用[]和 at()操作字符串
#include<string>
using namespace std;
int main(){
string s1 {"i love c++"};
cout << s1[3]<<endl;
cout << s1.at(0) << endl;
return 0 ;
}
- 遍历字符串
#include<string>
using namespace std;
int main(){
string s1 {"abcdef"};
for(char s : s1){
cout << s << endl;
}
for(int s : s1){
cout << s <<endl;
}
return 0 ;
}
- 字符串比较
字符串也是可以比较大小的。
#include<string>
using namespace std;
int main(){
string s1{"Apple"};
string s2{"Banana"};
string s3 {"kiwi"};
string s3 {"apple"};
string s3 {s1};
s1 == s5 // true
s1 == s2 // false
s1 != s2 // true
s1 < s2 // True
s1 > s2 // false
s1 == "Apple" // false
return 0 ;
}
- 截取字符串
#include<string>
using namespace std;
int main(){
substr(开始索引, 截取长度);
string s1 {"This is a test"};
cout << s1.substr(0 , 4) ; // This
return 0 ;
}
- 获取字符(字符串)在字符串中的索引
#include<string>
using namespace std;
int main(){
find(搜索的字符)
string s1 {"This is a test"};
cout << s1.find("This") ; // 0
cout << s1.find("is") ; // 2
cout << s1.find("test") ; // 10
return 0 ;
}
- 获取字符串长度
length() : 返回字符串长度
5. 练习
给定一串字符串,判定里面有多少个数字,多少个字母。并且把最后那个字母a修改为大写字母A
修改用的函数是:replace(修改起始索引位置,修改的横跨长度,修改成什么字符);
判断是否是字母和数字使用的是 isalpha () 和 isdigit() ,需要引入#include<ctype.h>
二、 Vector
Vector其实很大程度上和数组一样,只是数组是固定长度,而vector是不定长度(动态增长)。 假设我们需要记录明年的测试成绩,但是我们并不知道明年会有多少个学生。那么可以有两种选择,定义一个固定长度的数组,这个长度超过假设的长度, 另一种办法就是使用动态数组,比如是: vector
vector 在C++STL(标准模板库)中的一个容器,可以看成是对容器的一种扩展。在运行时可以改变长度 , 与数组具有相似的语法 , 相比数组更高效 , 提供越界检查
1. 声明和初始化
- 声明
使用vector除了要导入
#include
之外,由于它声明于std命名空间里面,所以要配合std命名空间使用
#include <vecotr>
using namespace std;
int main(){
vector <char> vowels;
vector <int> test_score;
// =========================
vector <char> vowels(5); //声明一个初始大小为5的char类型vector
vector <int> test_score(10);
return 0;
}
- 初始化
#include <vecotr>
using namespace std;
int mian(){
//数组定义
int test_score []{100,99,18,81}
//vector定义
vector <char> vowels {'a' , 'e' , 'i' , 'o' ,'u'};
vector <int> test_score{ 100 ,98,95,90,80};
vector <double> temperatures{26,20.7};
return 0;
}
2. 访问vector
访问
vector
中的元素有两种方式,一是仍以数组的方式,另一种是使用vector
提供的at
函数
- 数组的语法
#include <iostream>
#include <vector>
using namespace std;
int main(){
vector<int> test_score {100,90,85};
cout << "第一个成绩是: " <<test_score[0] << endl;
cout << "第二个成绩是: " <<test_score[1] << endl;
cout << "第三个成绩是: " <<test_score[2] << endl;
cout << "第三个成绩是: " <<test_score[3] << endl; //不会检查越界
return 0 ;
}
- vector的语法
#include <iostream>
#include <vector>
using namespace std;
int main(){
vector<int> test_score {100,90,85};
cout << "第一个成绩是: " <<test_score.at(0) << endl;
cout << "第二个成绩是: " <<test_score.at(1) << endl;
cout << "第三个成绩是: " <<test_score.at(2) << endl;
cout << "第三个成绩是: " <<test_score.at(3) << endl; //抛出越界异常
return 0 ;
}
3. 操作vector
- 修改vector中的元素
#include <vector>
using namespace std;
int main(){
vector<int> test_score {100,90,85};
test_score.at(0) = 73;
return 0 ;
}
- 往vector中追加元素
#include <vector>
using namespace std;
int main(){
vector<int> test_score {100,90,85};
test_score.push_back(80); // 100 , 90 , 85 , 80
test_score.push_back(95); // 100 , 90 , 85 , 80 , 95
return 0 ;
}
- 越界检查
只要当我们使用了vector的语法去获取超出索引的元素时,就会抛出异常。而使用数组的语法去获取元素,则不会进行越界检查
- 遍历vector
#include <iostream>
#include <vector>
using namespace std;
int main(){
//使用下标遍历
vector<int> scores{ 100 ,95 ,88 ,80 ,75};
for (int i = 0; i < scores.size(); ++i) {
cout << scores[i] << endl;
}
//基于范围for遍历
vector<int> scores{ 100 ,95 ,88 ,80 ,75};
for(int score : scores){
cout << score << endl;
}
return 0 ;
}
4. 二维vector
二维vector和二维数组实际上差不太多,二维数组是数组里面装的是数组,二维vector指的是vector里面装的还是vector,在未来碰到矩阵相关的存储操作,多半使用vector来作为媒介。 比如下面的例子,演示了使用vector来存储3个班的考试成绩。每个班的成绩单独使用一个vector来存储。
#include <iostream>
#include <vecotr>
using namespace std;
int main(){
//声明并初始化vector
vector<vector<int>> scores {
{95,77,80,85},
{58,89,93,100},
{69,73,81,97}
};
for (int i = 0; i < scores.size(); ++i) {
for (int j = 0; j < scores[i].size(); ++j) {
cout << scores[i][j] <<"\t" ;
}
cout << endl;
}
return 0 ;
}
5 . 练习
使用二维vector记录三个学生的6门学科成绩,并且计算每个学生的总分,平均分。
三、 函数
1. 函数介绍
在大多数地方,c++ 和 python的函数是一样的,都是用来包裹定义好的语句,避免重复拷贝粘贴。不过还是有些许不一样的地方。
- python的函数是以回车换行结尾,c++的函数是以 大括号结尾
- python的函数通常使用缩进方式来表示函数体, ,c++使用大括号区域来表示
- python是动态类型语言,而c++是静态类型语言,所以有时候需要像声明变量一样,声明函数。
2. 定义函数
函数的定义一般可以包含以下几个部分:
方法名称
、方法参数
、返回值
、方法体
, 根据可有可无的设置,函数一般会有以下4种方式体现。
- 声明并调用函数
#include <iostream>
using namespace std;
void say_hello(){
count << "hello" << endl;
}
int main(){
say_hello();
return 0 ;
}
1. 无返回值无参数
void say_hello(){
count << "你好 " << endl;
}
int main(){
say_hello();
return 0 ;
}
2. 无返回值有参数
#include<iostream>
using namespace std;
void say_hello(string name){
count << "你好 "<< name << endl;
}
int main(){
say_hello("张三");
return 0 ;
}
3. 有返回值无参数
#include<iostream>
using namespace std;
string say_hello(){
return "你好 张三";
}
int main(){
cout << say_hello() << endl;
return 0 ;
}
4. 有返回值有参数
#include<iostream>
using namespace std;
string say_hello(string name){
return "你好 "+ name;
}
int main(){
cout << say_hello("张三") << endl;
return 0 ;
}
3. 函数原型
般来说,c++的函数一般包含声明和定义两个部分。因为c++是静态类型语言,程序属于自上而下编译,所以在使用函数前,必须先表示函数的存在,告诉编译器函数所需要的参数以及函数的返回值是什么。
1. 函数定义在前
在调用函数之前,事先先定义好函数。
#include <iostream>
using namespace std;
//函数定义 ,函数的真正实现。
int add(int a , int b){
return a + b ;
}
int main(){
cout << add(1 ,2)<< endl;
return 0 ;
}
2. 使用函数原型
把函数分成声明和定义两部分,函数的原型定义在调用的前面,具体实现可以放在后面。
#include <iostream>
using namespace std;
//函数声明 ,也叫函数原型 并不知道这个函数具体是如何实现的。只是有一些基本架子而已。
int add (int a , int b);
int main(){
cout << add(1 ,2)<< endl;
return 0 ;
}
//函数定义 ,函数的真正实现。
int add(int a , int b){
return a + b ;
}
4. 分离式编译
一般说来,函数的声明 ( 函数原型 )通常都会放到头文件中,之所以称之为头文件是因为它总是在main函数的前面就引入进来。头文件一般以 .h 或者 .hpp 结尾,通常用于 写类的声明(包括类里面的成员和方法的声明)、函数原型、#define常数等,但一般来说不写出具体的实现
- math.h
为了能够让声明和定义能够快速的被关联上,通常它们的名称会被定义成一样的,这已经成为了一种默认的规定
//函数声明
int add (int a , int b);
- math.cpp
#include "math.h"
//函数定义 ,函数的真正实现。
int add(int a , int b){
return a + b ;
}
- main.cpp
#include <iostream>
#include "math.h" //这里使用"" 表示从当前目录查找
int main(){
add(1 ,2);
return 0 ;
}
5. 函数重载
在许多语言中,经常会见到两个或者两个以上的函数名称是一样的,当然他们的 参数个数 或者 参数类型 或者是 参数的顺序 是不一样的。这种现象有一个学名叫做 重载 overload, 由于python属于动态类型语言,不区分数据类型,参数可以是任意类型,所以它没有重载。
下面的示例代码即是对加法运行进行了重载,以便能够针对不同的数据类型,不同的参数个数做出匹配
int add(int a , int b){
return a + b ;
}
int add(int a , int b , int c){
return a + b + c;
}
int add(double a , double b){
return a + b ;
}
int main(){
add(3, 3);
add(3, 3, 3);
add(2.5 , 2.5);
return 0 ;
}
**思考: 为什么python没有函数重载? **
6. 函数参数
python的函数,在传递参数的时候,有可变对象和不可变对象的现象,那么在C++里面也有类似的说法。只不过是另一种说辞罢了。
python中传递不可变对象,在C++中,对应的是值的拷贝,也就是传递的只是数据的一份拷贝而已。在函数内部修改数据,并不会改变外部数据
python中传递可变对象,在c++中,对应的是引用传递,也就是传递的是对象的引用,而不是拷贝。在函数内部修改数据,会导致外部数据也发生改变。
1. 值传递
C++默认情况下,处理函数参数传递时,多数使用的是值的拷贝,少数部分除外。
#include<iostream>
using namespace std;
void scale_number(int num);
int main(){
int number{1000};
scale_number(number);
//打印number 1000
cout << number <endl;
return 0 ;
}
void scale_number(int num){
if(num > 100)
num = 100;
}
2. 传递数组
函数的参数除了能传递普通简单的数据之外,数组也是可以传递的。但是数组稍微有点特殊,这里多做讲解。
- 前面提过,形参实际上就是实参的一份拷贝,就是一个局部变量。
- 数组的数据太大,如果都进行拷贝,那么比较麻烦,也造成了浪费
- 所以实际上传递数组的时候,并不会进行整个数组的拷贝,而只是传递数组的第一个元素内存地址 (指针 ) 进来。
- 数组的数据还是在内存中,只是把第一个元素(也就是数组的起始)内存地址传进来而已。
- 这就造成了函数的内部根本无法知道这个数组的元素有多少个。
#include<iostream>
using namespace std;
using namespace std;
//传递数组长度
void print_array(int number[] , 5 );
int main(){
//声明数组
int array []{1,2,3,4,5};
//打印数组
print_array(array , 5);
return 0 ;
}
//传递数组,打印数组
void print_array(int array[] , int size){
for (int i {0} ; i < size ; i++){
count << array[i] << endl;
}
}
3. 传递引用
目前为止,我们所有函数的参数传递,都是对数据进行了一份拷贝(数组除外)。那么在函数的内部是不能修改值的,因为这仅仅是一份值得拷贝而已(函数外部的值并不会受到影响)。如果真的想在函数内部修改值,那么除了数组之外,还有一种方式就是传递
引用
。引用实际上只是原有数据的一种别名称呼而已,使用
&
定义
#include<iostream>
using namespace std;
void scale_number(int &num);
int main(){
int number{1000};
scale_number(number);
//打印number100
count << number <endl;
return 0 ;
}
void scale_number(int &num){
if(num > 100)
num = 100;
}
4. 练习
既然掌握了使用传递引用,以便修改外部数据,那么来修改下传递vector吧。vector和普通的数据一样,传递的时候,做的是值得拷贝。数据有点浪费,只想传递引用,怎么办呢?
有一个装有6个学科分数的vector,请把这个vector传给另一个函数change_score()函数,在该函数内部 请使用基于范围的for循环对vector进行遍历,把vector里面所有低于60分的分数,修改为:100分。
7 . 函数是如何被调用工作的
- 函数是使用函数调用栈来管理函数调用工作的。
- 类似盒子的栈
- 遵循后进先出
- 可以往里面执行压栈和出栈动作(push 和 pop)
- 栈的结构和激活记录
- 函数必须把它的返回值返回给调用它的函数(A —> B)
- 每次函数的调用都需要创建一次激活记录,然后把它压入栈中(push)
- 当一个函数被调用完毕的时候,就需要从栈中弹出(pop)
- 函数的参数以及内部的局部变量都是存储在栈中。
- 函数栈有可能抛出栈溢出异常(Stack Overflow)
- 一旦函数调用栈中被push进来的函数记录过多,就有可能出现。(例如:无限循环调用 | 递归 )
void func2(int &x , int y , int z){
x +=y+z;
}
int func1(int a , int b){
int result{};
result += a + b;
func2(result , a , b );
return result ;
}
int main (){
int x{10};
int y{20};
int z{};
z = func1(x , y );
cout << z << endl;
return 0 ;
}
8 . 内联函数
函数可以使我们复用代码,但是一个函数的执行,需要开辟空间、形参和实参进行值得拷贝,还要指明函数返回、以及最后回收释放资源的动作,这个过程是要消耗时间的。
- 作为特别注重程序执行效率,适合编写底层系统软件的高级程序设计语言,如果函数中只有简单的几行代码,那么可以使用
inline
关键字来解决了函数调用开销的问题
#include<iostream>
inline int calc_Max (int a, int b)
{
if(a >b)
return a;
return b;
}
int main(){
int max = calc_Max(3, 8);
std::cout << "max = " << max << std::endl;
return 0 ;
}
增加了 inline 关键字的函数称为“内联函数”。内联函数和普通函数的区别在于:当编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数体的代码插人调用语句处,就像整个函数体在调用处被重写了一遍一样。
有了内联函数,就能像调用一个函数那样方便地重复使用一段代码,而不需要付出执行函数调用的额外开销。很显然,使用内联函数会使最终可执行程序的体积增加。以时间换取空间,或增加空间消耗来节省时间,这是计算机学科中常用的方法。
9. 范围规则
在学习过程,我们会定义很多变量或者引用、这些变量由于定义的位置不同,所以它们的作用域范围也不同。一般会划分成几种类型:
代码块
|局部变量
|静态变量
|全局变量
- 单独代码块
#include<iostream>
using namespace std;
int main(){
int num{10};
int num1{20};
cout << num << num1 << endl;
{
int num{100};
cout << "num = "<< num << endl;
cout << "num1 = "<< num1 << endl;
}
}
- 函数中局部变量
#include<iostream>
using namespace std;
int num{300};
void local_example(int x){
int num{1000};
cout << "num =" << num << endl;
num = x ;
cout << "num =" << num << endl;
}
- 静态本地变量
- 只会初始化一次
- 重复调用函数也只会初始化一次。
#include<iostream>
using namespace std;
void static_local_example(){
static int num{100};
cout << "num ="<< num << endl;
num+=100;
cout << "num ="<< num << endl;
}
int main(){
static_local_example();
static_local_example();
return 0 ;
}
- 全局变量
通常声明在所有函数和类的外部 ,若存在局部变量和全局变量同名情况下,可以使用 域操作符
::
来访问全局变量
#include<iostream>
using namespace std;
int age = 99;
int main(){
int age =18 ;
cout << ::age << endl;
return 0 ;
}
10. 打卡作业
- 有两个字符串 A , B , A 为源字符串,B 为要删除的字符串,判断A是否包含B ,如果包含,请把A 里面包含B的字符删除后,输出全新的字符串A,否则直接输出源字符串A。
string a = "ab123def78cc09"; //源字符串
string b = "cc";//要删除字符串
字符串的删除函数: a.delete() | a.remove() | a.erase() 这三个函数,必有一个!
-
定义一个计算器,提供加减乘除功能
-
定义一个calc.h 作为加减乘除 四个函数的声明
-
定义一个calc.cpp 作为计算器的具体实现
-
定义一个main.cpp 作为程序的入口
-
使用二维vector 用于保存张三、李四、王五,三人的6个学科的成绩,
-
定义一个二维vector,名叫:score_vector,用来存储三个学生的6个学科成绩
-
定义一个initScore(vector<vector>)函数负责从键盘录入成绩 获取3个人,6个学科成绩
-
定义一个函数updateScore(vector<vector>) ,用于更新分数,把每个人的不及格的成绩全部修改成99分,
-
定义一个函数 printScore (vector<vector>),遍历打印三个人的每个学科成绩。
-
要求使用函数原型、分离式写法(头文件和源文件) 、函数传递引用,基于范围for循环。
-
禁止定义全局静态vector ,要求2 ,3 ,4 步骤的函数要携带参数,把二维vector传递进去。
-
超纲: 应该要考虑引用,否则更新的操作无法实现。具体使用可看上面的引用。
-
应该有3个文件 stu.h 、stu.cpp , main.cpp
- stu.h :用于声明三个函数
- stu.cpp用于实现三个函数
- main.cpp 用于程序的执行入口
vector<vector<int>> score_vector;
int main (){
initScore( score_vector ); //往这个二维vector里面装成绩
updateScore( score_vector ); // 遍历二维vector,更新成绩 【必须要用引用 | 指针】
printScore( score_vector ); //打印成绩。
//超纲: 引用。
return 0 ;
}
day04/05
一、 指针
1. 什么是指针
int a = 3;
int 指针 = 内存地址 (十六进制的数据);
指针其实就是一个变量,不过它的值是一个内存地址 , 这个地址可以是变量或者一个函数的地址
当你声明明一个变量的时候,计算机会将指定的一块内存空间和变量名进行绑定;这个定义很简单,但其实很抽象,例如:int x = 5; 这是一句最简单的变量赋值语句了, 我们常说“x等于5”,其实这种说法是错误的,x仅仅是变量的一个名字而已,它本身不等于任何值的。这条statement的正确翻译应该是:“将5赋值于名字叫做x的内存空间”,其本质是将值5赋值到一块内存空间,而这个内存空间名叫做x。切记:x只是简单的一个别名而已,x不等于任何值。
- 为什么需要指针?
不就是使用变量或者调用函数吗?难道不能直接调用吗?那么需要指针做什么呢?
实际上是可以的,但是并不是所有的情况都可以。比如:
1. 在内部函数中,可以使用指针访问外部函数中定义的某个变量x, 因为它并不是声明在自己的函数范围内。
2. 指针在处理函数传递数组的时候非常高效
3. 我们还可以在堆内存中申请一块动态内存,这块内存甚至没有一个变量名称,唯一的访问方式是通过指针。
4. 可以使用你指针访问指定的内存地址(游戏修改器) 内存修改器 | 金山游戏 | CE 。。。
2. 指针使用
1. 声明指针
1.声明指针的时候要记得初始化,如果没有初始化,指针存放的将会是垃圾数据(因为你根本不知道它指向何方)
- 可以使用nullptr(c++11)进行指针初始化,初始化存放的值是 0
变量类型 *指针名称;
int * int_ptr;
double * double_ptr;
char * char_ptr;
string * strng_ptr;
2. 初始化指针
指针的指向是一个块内存地址,如果仅仅是声明而未初始化,那么指针的指向无法得到保证,如果没有明确、指针的指向,那么最好把它初始化成一个空指针,也就是表示目前没有指向,空指针的值是0
//初始化指针
int a = 3 ;
int *p0 = &a; //p0是一个指针,指向的是一个int类型的数据,这个数据是3.
//空指针
int *p1 = nullptr;
int *p2 = NULL;
int *p3 = 0 ;
3. 指针地址和大小
指针实际上也是一个变量,也会有自己的内存空间,也会有自己的长度大小。获取指针的内存地址,依然使用取地址符
&
, 长度大小依然使用sizeof
来获取
int age = 88;
int *p = &age;
cout << "指针的地址是: " << &p <<endl;
cout << "指针存储的是: " << p <<endl;
cout << "指针大小是: " << sizeof p <<endl;
cout << "age的大小是: " << sizeof age <<endl;
3. 指针dereference( 解引用)
所谓的指针dereference就是,指针就是一个变量,存放的是一个地址。这个地址有可能是变量 a 或者是变量b的地址。有了这个地址,我们可以通过dereference操作符
*
去获取到a对应的值或者b对应的值。
#include<iostream>
using namespace std;
int main(){
//定义一个变量score,赋值100
int score {100}
//定义一个指针score_ptr 指向score的地址。
int *score_ptr{&score};
//通过指针,获取到指向位置的数据 打印100
cout << *score_ptr << endl;
//使用指针修改原来的score
*score_ptr = 200 ;
//使用指针和变量的方式打印score,结果都输出200
cout << *score_ptr << endl;
cout << score << endl;
return 0 ;
}
练习
定义一个函数,用于交换两个变量的值。请使用指针来完成
void changeValue( int a,int b){ //变量a ? 还是指针? 不要写引用。
//在这里面更改你的值。
}
int main(){
int a = 3 ;
int b = 4;
changeValue(a , b);
cout <<"a =" << a << endl; // 4
cout << "b = " << b <<endl; //3
return 0 ;
}
4. 动态内存分配
在进行编码的时候,我们根本不知道需要多少内存空间。举个例子,比如我们需要存储学生的数据,这时候可以使用数组来存储,那么就必须知道学生的具体人数。如果不知道,就无法使用数组了。实际上之前学过的vector就是使用动态内存。但是有时候,我们如果需要存放的是一单个对象数据,并不是一堆数据。用vector就有点浪费了。
为了解决上述问题,C++ 提供了一种“动态内存分配”机制,使得程序可以在运行期间,根据实际需要,要求操作系统临时分配一片内存空间用于存放数据。此种内存分配是在程序运行中进行的,而不是在编译时就确定的,因此称为“动态内存分配”。申请动态内存
1. 申请内存
可以使用
new
关键字来申请动态内存 ,new
开辟出来的空间都位于堆内存中。
#include<iostream>
using namespace std;
int main(){
//定义一个int类型的指针,并没有指向任何地方。
int * int_ptr{nullputr};
//在堆中申请内存,使用指针指向
int_ptr = new int ;
//由于未赋值,所以输出的可能是未知的值
cout << *int_ptr << endl;
//修改开辟空间的数据值为100
*int_ptr = 100;
//解引用,输出100
cout << *int_ptr << endl;
return 0 ;
}
2. 释放内存
new
常和delete
成对出现,使用new
开辟空间, 使用delete
释放申请的内存,避免造成内存泄漏
#include<iostream>
using namespace std;
int main(){
int *int_ptr{nullptr};
int_ptr = new int ; //申请内存
...
delete int_ptr ; //释放内存
return 0 ;
}
3. 数组操作
使用
new int[]
来给数组申请动态内存 , 然后使用delete[]
释放申请的内存
#include<iostream>
using namespace std;
int main(){
int *array_ptr{nullptr};
int size{};
cout << “你期望的数组大小是:” << endl;
cin >> size ;
array_ptr = new int[size];
//释放申请的空间
delete [] arrray_ptr;
return 0 ;
}
4. 关于动态内存的思考
通常情况下,定义的变量存储的位置位于栈内存中,栈内存的数据,当函数执行结束后即会被释放,这是栈内存的机制自己决定的,并且栈内存中由于内存并不是太大,所以不建议大量的数据存放在栈内存。
而堆内存中的容量相比栈内存要大多了,但是堆内存并不提供回收释放的工作,允许程序申请内存空间,但是同时也要自己负责内存空间的释放工作。
不能一概而论哪一种是最优的决定,要根据开发场景来决定。
如果你想让这份数据存留的时间更长,不以函数的执行结束而销毁,那么就需要使用堆内存来存放。int *a = new int;如果这份数据是与函数共存亡,其他地方也用不上,那么就直接定义到栈内存里面去就行。 int a = 3;
5. 数组和指针的关系
数组其实和指针是存在一些内在联系的,如下:
- 根据数组名字取到的内存地址,是数组的第一个元素地址
- 指针其实是一个变量,这个变量存放的值是内存地址
- 如果一个指针和数组是同样的类型,并且指针存放的地址正好是数组的某个元素地址,那么可以通过该指针操作数组
1. 数组与指针
#include<iostream>
using namespace std;
int main(){
//定义3个长度的int类型数组
int scores []{100, 95 , 98};
//直接打印数组,实际上是打印数组第一个元素的地址 0x61fec8
cout << scores << endl;
//使用*操作符是根据地址获取数据,所以取到的是第一个元素 : 100
cout << *scores << endl;
//声明指针,存放的是数组第一个元素的地址
int *score_ptr{scores};
//打印指针,其实输出它保存的地址,即数组首元素地址 0x61fec8
cout << score_ptr << endl;
//解引用,输出的是数组的首元素 100
cout << *score_ptr << endl;
return 0 ;
}
2. 指针运算
如果指针指向的是数组的第一个元素地址,那么同样可以通过对指针进行加减运算,来获取其他的元素 。值得注意的是,指针的相加并不是单纯数字上的相加,而是指针对应类型占用字节的相加
#include<iostream>
using namespace std;
int main(){
//定义3个长度的int类型数组
int scores []{100, 95 , 98};
//定义一个int类型指针,指向的是数组的首元素
int *score_ptr{scores};
//使用数组的手法打印数组
cout << score_ptr[0] << endl; //100
cout << score_ptr[1] << endl; //95
cout << score_ptr[2] << endl; //98
//对指针进行加法运算。由于score_ptr 是int类型,
//而int类型占用4个字节,所以每次相加打印出来的地址都会变长4个字节
cout <<score_ptr << endl; // 0x7fffacde9420
cout <<(score_ptr+1) << endl; // 0x7fffacde9424
cout <<(score_ptr +2) << endl; // 0x7fffacde9428
//指针解引用取值
cout <<*score_ptr << endl; // 100
cout <<*(score_ptr+1) << endl; // 95
cout <<*(score_ptr +2) << endl; // 98
return 0 ;
}
6. 指针算数
指针除了表示存储的是内存地址之外,它也可以做算术运算 和 比较大小。
指针可以用关系运算符进行比较,如 ==、< 和 >。如果 p1 和 p2 指向两个相关的变量,比如同一个数组中的不同元素,则可对 p1 和 p2 进行大小比较。
1. 指针递增
指针递增 , 如果是数组操作,可以指向后一个元素
int score[]{90,85,100};
int * score_ptr{score}
for(int i{0} ; i<3 ; i++){
cout << ptr << endl;
cout << *ptr << endl;
ptr++; // 指针移动指向下一个元素
}
2. 指针递减
指针递减 , 如果是数组操作,可以指向前一个元素
int score[]{90,85,100};
int * score_ptr;
score_ptr = score[2]; //指针指向的是数组最后一个元素内存地址
for(int i{3} ; i>0 ; i--){
cout << ptr << endl;
cout << *ptr << endl;
ptr--; // 指针移动指向下一个元素
}
3. 等价判断
两个指针的等价判断,实际上是他们指向的地址比较
#include<iostream>
using namespace std;
int main(){
string s1{"张三"};
string s2{"张三"};
string *p1 {&s1};
string *p2 {&s2};
string *p3 {&s1};
cout << (p1 == p2) <<endl; //false
cout << (p1 == p3) <<endl; //true
cout << (*p1 == *p2) <<endl; //true
cout << (*p1 == *p3) <<endl; //true
return 0 ;
}
4. 练习
有一个int类型数组,存储着张三的6个学科成绩,其中有的学科成绩不及格,现在想使用updateScore() 函数来更新不及格的分数为100分。updateScore (int count , int [] scores) count : 为数组的长度, scores 用于接收数组。 在函数内部使用指针运算,修改分数。
7. 指针与常量
1. 指针常量
const int *p
表示指针指向常量 , 不允许修改对应的值,但是可以指向别的地方, 这和常量修改值一样。
int main(){
//定义高分和低分的变量
int high_score{100};
int low_score{75};
//使用指针常量指向高分。这里的const修饰的是指向的数据
const int *score_ptr {&high_socre};
//不允许修改值,因为此时编译器会认为high_score 是一个常量
*score_ptr = 86 ; // 错误
//可以指向其他位置。
score_ptr = &low_score ; // 正确
//当然也可以通过变量方式来修改值。
high_score = 88;
return 0 ;
}
2. 常量指针
int* const p
表示这个指针是常量指针,不能再指向别的地方了,但是可以修改目前指向地方的值
int main(){
int high_score{100};
int low_score{75};
//表示这个指针是一个常量,这里的const修饰的是指针
int *const score_ptr{&high_score};
//允许修改值,但是不允许再做其他指向
*score_ptr = 86 ; // 正确
score_ptr = &low_score ; // 错误
return 0 ;
}
3. 常量指针指向常量
const int* const p
表示这个指针是常量指针,并且它指向的位置的值也是常量
int main(){
int high_score{100};
int low_score{75}l
//第一个const是修饰指向的数据,第二个const是修饰指针。
//表示不管是指针还是指向的数据,都是常量
const int *const score_ptr{&high_score};
//既不允许修改指向,也不允许修改指向的值
*score_ptr = 86 ; // 错误
score_ptr = &low_score ; // 错误
return 0 ;
}
8 打卡作业
-
定义结构体stu, 包含学生的姓名(字符串)、学号(字符串)、成绩(vector)6个学科
-
定义一个vector来保存张三、李四、王五三个学生的信息 ,请使用学生的指针类型。。
- vector< stu* > stu_vector;
-
三个学生采用new的方式创建出来,保存在堆内存中。
-
定义initScore函数负责录入学生的信息。 姓名 , 学号 , 6个学科成绩
-
定义updateScore函数负责更新学生的沉寂。成绩小于60的,更新为60
-
定义printScore函数打印学生的信息: 姓名、学号、总分、平均分
-
使用分离式写法: stu.h , stu.cpp , main.cpp
- stu.h 包含 结构体stu的声明, 三个函数的声明
- stu.cpp 包含三个函数的具体实心。
- main.cpp 作为程序的入口
-
三个函数传递vector的时候,请使用指针类型传递。
initScore(vector & score_vector)
initScore(vector <stu*> *score_vector)
二、指针与函数
1. 参数传递指针
函数的参数,除了传递普通的变量,引用之外,还可以把指针当成参数来传递
#include <iostream>
using namespace std;
//函数原型
void double_data(int *int_ptr);
int main(){
int value{10};
//修改前,输出为10
cout << value << endl;
//在函数内部修改value的值
double_data(value);
//修改后,输出为20
cout << value << endl;
}
void double_data(int *int_ptr){
*int_ptr *=2;
}
在某些情况下,传递指针比其他方式的传递要合适得多,比如下面有一个函数负责交换传递进来的两个参数的值, 此时如果不使用指针或者引用,则无法实现该功能
#include <iostream>
using namespace std;
void swap(int *a , int *b);
int main (){
int x{100},y{200};
//交换前打印 x : 100 , y : 200
cout << x <<" = " << y <<endl;
swap(&x , &y)
//交换前打印 x : 200 , y : 100
cout << x <<" = " << y <<endl;
}
void swap(int *a , int *b){
int temp = *a ;
*a = *b ;
*b = temp;
}
2. 函数返回指针
函数平常除了返回标准的数值之外,其实也可以返回指针。
#include <iostream>
using namespace std;
//返回两个参数中的最大者
int *calc_largest(int *ptr1 , int *ptr2);
int main(){
int a = 100;
int b = 200;
//使用指针指向最大数值
int *largest_ptr = calc_largest(&a , &b);
//输出:200
cout << *largest_ptr << endl ;
return 0 ;
}
int *calc_largest(int *ptr1 , int *ptr2){
//解引用获取到数据后比较 :
if(*ptr1 > *ptr2){
return ptr1;
}else{
return ptr2;
}
}
注意: 不要返回一个函数内部的一个局部变量指针 , 因为本地变量的声明周期应该只位于函数内部。一旦函数执行完毕则被释放。
#include <iostream>
using namespace std;
int *do_this(){
int size = 10;
return &size;
}
int main(){
int *result = do_this();
std::cout <<"result = " <<result << std::endl;
return 0 ;
}
3. 二级指针
指向指针的指针,即可称之为二级指针。有点类似二维数组,数组里面装的是数组,即可称之为二维数组。
1. 二级指针介绍
2. 二级指针的应用
二级指针不如一级指针使用的那么频繁,通常出现的地方是作为函数参数传递。如果在函数的内部想要修改外部一级指针指向的数据值,那么则需要二级指针了。
如下示例所示, 如果传递的是一个一级指针,那么在外部的p依然没有被分配空间,传递进去的依然是一份值的拷贝而已 , 导致后面的指针赋值 为 42 也无法执行。
//函数原型
void createPointer(int** p);
int main(){
//空指针
int* p = nullptr;
//在函数内部对该指针初始化
createPointer(&p);
//初始化完毕后,修改指向空间的数据
*p = 42;
//释放指针
delete p ;
return 0 ;
}
void createPointer(int** p){
// 解引用,得到的是一级指针,其实就是得到了外面的 p 那么整段话连起来就是 int *P = new int();
*p = new int();
}
3. 练习
有一个vector指针, 用于存储3个学生张三、李四、王五, 该vector的指针创建工作交由createContainer 函数来创建,并且该函数没有返回值。通过函数参数的方式,来创建外部vector指针。
4. 函数指针
1. 基本使用
函数指针的意思是指向函数的指针 。 通常来说,指针是变量,有自己的类型,那么函数指针也有类型。只不过它的类型稍微不一样而已。函数指针的类型由函数的返回值、函数的参数列表决定。 要想声明一个函数指针,只需要使用
指针
来替换函数名
即可。
- 普通方式调用函数
#include <iostream>
//函数原型
int add(int a , int b);
int main(){
int result = add(3,4);
std::cout << "result = " << result << std::endl;
return 0 ;
}
int add(int a , int b){
return a + b;
}
- 使用函数指针方式调用函数
#include <iostream>
using namespace std;
//函数原型
int add(int a , int b);
int main(){
//前半段表示声明一个函数指针add_ptr 该函数指针指向的函数返回值是int,并且有两个int类型的参数。
//指针的小括号不能省略。
int (*add_ptr) (int,int) =add;
//普通方式调用函数
int result =add(3,4);
cout << "result = " << result << endl;
//使用函数指针方式调用add函数
int result2 = add_ptr(3,4) ;
cout << "result2 = " << result2 << endl;
return 0 ;
}
int add(int a , int b){
return a + b;
}
2. 函数指针作为参数
有时候,也可以把某个函数A通过参数的方式传递给另一个函数B,随后在函数B里面执行传递进来的函数A。函数虽然不能直接作为参数来进行传递,但是函数指针可以。实际上在传递的时候,传递的是指针而已。
比如下面的示例: 有一个计算的函数
calc
, 允许在第三个参数传递进来具体的计算函数。
#include<iostream>
using namespace std;
int add (int a , int b);
int calc(int a , int b ,int (*fun)(int, int));
int main() {
//函数指针p,指向add函数
//int(*p)(int ,int) = add;
//cout << calc(3,5 , p) << endl;
//函数名称可以直接使用,它实际上就是一个函数指针。
cout << calc(3,5 , add) << endl;
return 0 ;
}
int add (int a , int b){
return a + b;
}
//计算的函数,最后的参数要求的是一个函数指针。
int calc(int a , int b ,int (*fun)(int, int)){
return fun(a,b);
}
3. 函数指针的作用
如果一个通用的函数,需要使用到 另一个函数,但是这个函数并没有确定名称,是由其他组织或者个人开发的,那么这时候可以预留一个位置,做成函数指针 虚位以待。比如:现在有一个vector或者数组,需要交给其他个人或组织来遍历,但是这些组织或者个人的遍历手法不一样,那么这时候可以使用函数指针占位。
//函数原型
void listScore(vector<int> scores , void (*ps)(vector<int>));
void printScore1(vector<int> scores);
void printScore2(vector<int> scores);
int main() {
vector<int> scores {50,60,70,80,90};
listScore(scores , printScore1);
listScore(scores , printScore2);
return 0 ;
}
//接收函数指针的函数
void listScore(vector<int> scores , void (*ps)(vector<int>)){
ps(scores);
}
//打印函数1
void printScore1(vector<int> scores){
cout << "****采用基于范围for循环遍历******" << endl;
for(int i : scores){
cout << i << endl;
}
}
//打印函数2
void printScore2(vector<int> scores){
cout << "****采用for i循环遍历******" << endl;
for (int i = 0; i < scores.size(); ++i) {
cout <<scores[i] << endl;
}
}
4. typedef使用
typedef 的作用是自己习惯的名字,来替已有的类型名称。 语法:
typedef 已知类型名称 自定义名称
#include<vector>
using namespace std;
//使用myint 来替代 int。
typedef int myint ;
//使用vi 来替代 vector<int>
typedef vector<int> vi;
int main(){
int a = 3 ;
//此时的myint 等价于 int
myint b = 3 ;
vector<int> scores1{60,70,80,90}
//此时的vi 等价于 vector<int>
vi scores2{60,70,80,90};
return 0 ;
}
1. 未使用 typedef 简化
下面示例代码,没有使用
typedef
简化函数指针
#include<vector>
using namespace std;
void listScores(vector<int> s , void(*pfs)(vector<int>)){
pfs(s);
}
void pirntScores(vector<int> scores){
//遍历打印
for(int s: scores){
cout << s<< endl;
}
}
int main(){
vector<int> scores {60,70,80,90};
//把printScores函数传递给listScores , 函数名称单独使用,它实际上是一个函数指针
listScores( scores ,pirntScores);
return 0 ;
}
2. 使用 typedef 简化
下面示例代码,使用
typedef
简化函数指针 , 实现的功能都是一样的,只是代码简化了些
#include<vector>
using namespace std;
// 声明一种类型,这种类型的名称叫做 pfs ,它是一种函数指针,该函数没有返回值,有两个分别是int的参数
typedef void(*pfs)(vector<int>);
//此处使用简化好的函数指针类型
void listScores(vector<int> s , pfs p){
p(s);
}
void pirntScores(vector<int> scores){
//遍历打印
for(int s: scores){
cout << s<< endl;
}
}
int main(){
vector<int> scores {60,70,80,90};
//也可以选择使用变量来接收printScores
pfs pfs1 = pirntScores;
//把pfs1 传递给listScores ,实际上传递的是printScores
listScores( scores ,pfs1);
return 0 ;
}
5. 练习
设计一个计算器,具有增删改查函数,还有一个函数operator。该函数接收三个参数,分别是: a, b , 和最终执行的具体操作函数的指针。最终通过operator来调用对应的算术操作,举例如下:
operator( 3, 4, , add) ; // 执行加法运算
operator(3,4 , sub) ; //执行减法运算
operator(3,4 , mul); //执行乘法运算
operator(3,4,div) ; //除法运算。
其中第三个参数,是函数指针。
三、引用
1. 什么是引用
引用,顾名思义是某一个变量或对象的别名,对引用的操作与对其所绑定的变量或对象的操作完全等价。引用在使用时,有几个要注意的地方:
- &不是求地址运算符,而是起到标志作用
- 引用的类型和绑定的变量类型必须相同 (指针也一样)
- 声明引用的同时,必须对其进行初始化,否则报错。
- 引用相当于变量或者某个对象的别名,但是不能再将已有的引用名作为其他变量或者对象的名字或别名 (和指针不一样。)
- 引用不是定义一个新的变量或对象,因此内存不会为引用开辟新的空间存储这个引用
- 不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。
#include<iostream>
using namespace std;
int main(){
//语法:类型 &引用名=目标变量名;
int a = 3 ;
int &b = a ;
//此时a也会变成33
b = 33;
vector<string> names {"张三" , "李四" ,"王五"};
// 这仅仅是改变了str的值,并不会改变vector里面的元素
for(auto str:names){
str = "赵六";
}
//此处不会产生新的变量,所以修改的都是vector里面的元素
for(auto &str : names){
str = "赵六" ;
}
return 0 ;
}
2. 左值和右值
C++的表达式要么是左值
lvalue
,要么是右值rvalue
这两个名词是从C语言继承过来的。左值可以出现在赋值语句的左侧和右侧,右值只能出现在右侧。最长见到左值和右值的地方,是在函数的参数以及报错的日志信息里面。不能简单的以等号的左右来判断是否是左值还是右值
判断是否是左值,有一个简单的办法,就是看看能否取它的地址,能取地址的就是左值 。使用排除法,其他的即为右值。
左值可看作是 对象,右值可看作是 值 而有时候对象也可以是左值也可以右值,一句话概括:当一个对象成为右值时,使用的是它的值(内容) , 而成为左值时,使用的是它的身份(在内存中的位置)。
//只能把左值赋值给引用,不能把右值赋给引用
int square(int &n){
return n * n ;
}
int main(){
int x = 1000; // x是一个左值, 而 1000 是一个右值。
x = 1000 + 20 ; //x 是左值, 1000 + 20 是右值,
int b = 10; // b 是一个左值, 10 是一个右值
b = x ; //b是一个左值, 而 x依然是一个左值。
int num = 10 ;
square(num) ; //正确
square(5) //错误。因为5是右值 ,不能赋值引用。
return 0;
}
3. 左值引用
平常所说的引用,实际上指的就是左值引用
lvalue reference
, 常用单个&
来表示。 左值引用只能接收左值,不能接收右值。const 关键字会让左值引用变得不同,它可以接收右值
#include <iostream>
using namespace std;
//函数原型
int add(int &num1);
void print(vector<int> &scores);
int main(){
int a = 3 ;
int &b = a ; //ar是一个左引用,实际上可以看成是a的一个别名。
// 这是不允许的。
// 1. 从引用层面理解的话是: 引用接收的一个变量,给某个变量起别名
// 2. 从左右值的层面理解是,这是一个左值引用,只能接收左值。 3 属于右值。
int &c = 3 ; //错误!
int a = 3 ;
add(a) ; //正确
add(3) ; //错误! 参数要求的是一个左值引用,只能赋值左值 ,3 属于右值
vector<int> scores{60,70,80,90};
print(scores); //正确
print({60,70,80,90}); //错误!
}
4. 右值引用
为了支持移动操作,在c++11版本,增加了右值引用。右值引用一般用于绑定到一个即将销毁的对象,所以右值引用又通常出现在移动构造函数中。
看完下面的例子,左值和右值基本就清楚了,左值具有持久的状态,有独立的内存空间,右值要么是字面常量,要么就是表达式求值过程中创建的临时对象
int main(){
int i = 66;
int &r = i ; //r 是一个左引用,绑定左值 i
int &&rr = i ; //rr是一个右引用,绑定到左值i , 错误!
int &r2 = i*42 ; // r2 是一个左引用, 而i*42是一个表达式,计算出来的结果是一个右值。 错误!
const int &r3 = i*42; // 可以将const的引用,绑定到右值 正确
int &&rr2 = i*42 ; // 右引用,绑定右值 正确
return 0 ;
}
- 示例
#include<vector>
using namespace std;
//函数原型
int add(int &&num1);
void print(vector<int> &&scores);
int main(){
int a = 3 ;
add(a) ; //错误!参数要求的是一个右值引用,只能赋值右值
add(3) ; //正确!
vector<int> scores{60,70,80,90};
//print接收的是一个右值,此处的scores是一个左值。
print(scores); //错误!
//{60,70,80,90} 属于右值。
print({60,70,80,90}); //正确
return 0 ;
}