C++基础
1.初识C++
1.1标准输出和输入
基本语法:
- 标准输入
cin >> a;
- 标准输出
cout << "文字提示" << 变量或表达式 << endl;
这里endl
相当于回车
在使用 cin
和 cout
时,要在源文件最上方加上 #include <iostream>
和 using namespace std;
1.2注释
分为单行注释和多行注释:
- 单行注释用
//
- 多行注释用
/* */
1.3变量
定义为数据类型+变量名+赋初值
1.4常量
定义方式有两种
- #define宏常量,通常定义在文件最上方
- const修饰的变量,一般定义在函数内部
1
#define day 7
int main()
{
const int month = 12; //const定义的常量
}
两种定义的常量都是不能被修改的,否则会报错。
1.5关键字
在给变量和常量起名字时,不要用关键字作变量名。
1.6标识符命名规则
- 标识符不能是关键字
- 标识符只能由字母、数字、下划线组成
- 第一个字符必须是字母或下划线,不能是数字
- 标识符中的字母区分大小写
给变量起名字时,最好做到见名知意。
2.数据类型
2.1整型
数据类型 | 占用空间 | 取值范围 |
---|---|---|
short(短整型) | 2字节 | -32768~32767 |
int(整型) | 4字节 | -2147483648~2147483647 |
long(长整型) | win下4字节、Linux下4字节(32位)、8字节(64位) | -2147483648~2147483647 |
long long(长长整型) | 8字节 | -263到263-1 |
**如果超出范围会发生一些有意思的事,比如:**short a=32768;实际上a打印出来为-32768,从上限反转到下限。
其中int最为常用。
2.2 sizeof()
sizeof(数据类型/变量),返回是字节数
2.3浮点型
分为 float 和 double ,单精度占4个字节、双精度占8个字节,区别就是有效数字位数不同,不管是单精度还是双精度在打印时只显示6位有效数字
科学计数法:3e2 意思是3*10^2=300,e之后的数字表示10的多少次方。
2.4字符型
字符型变量占用1个字节空间,字符型变量并不是把字符本身放到内存中去存储,而是将对应的 ASCII 编码放入存储单元,想要打印 ASCII 的值可以用 (int)a 来转换。可以直接用 ASCII 给字符型变量赋值如:ch = 97;
常见的一些错误如下:
- 创建字符型变量时要用单引号,不要用双引号
- 单引号内部只能写一个字符
2.5转义字符
常用的几个如下:
- \n 换行符
- \ 转义输出一个反斜杠,只用一个反斜杠会报错,第一个反斜杠表示我要输出一个特殊字符了,第二个反斜杠代表我这个要输出的特殊字符就是反斜杠
- \t 水平制表符,作用是可以使输出变得整齐
2.6字符串型
两种风格
- C风格字符串:char 变量名[] = “字符串值”;注意要加中括号,并且用双引号
- C++风格字符串:string 变量名 = “字符串值”;注意用的时候必须要包含一个头文件,
#include<string>
2.7 bool类型
bool类型只占用一个字节的空间,只有两个值 true 或 false 本质分别为1和0。
2.8数据的输入
主要语法用到 cin >> 变量
,cin可以为整型、浮点型、字符型,字符串型、bool型(只要是输入非0的值,都会赋值成1)赋值。
3.运算符
3.1算术运算符
加减乘除,应当注意的是除法运算,两个整型数相除结果依然是整型,0不能作除数否则报错。
取模运算即取余数使用 % ,应当注意的是 0不能作为取余运算的右操作数,且两个小数不能作取余运算。
前置递增和后置递增,都可以让变量加1,但是它们的区别是:前置递增是先让变量+1,再进行表达式的运算,后置递增是先进行表达式的运算,再让变量+1。前置递减和后置递减也是同样的道理。
3.2赋值运算符
举例如下:
int a = 10;
a = 100; //普通赋值
a += 2; //等价于a = a+2
a -= 3; //等价于a = a-3
a *= 4; //等价于a = a*4
a /= 5; //等价于a = a/5
a %= 6; //等价于a = a%6
3.3比较运算符
主要有大于、小于、等于、不等于、大于等于、小于等于。比较运算表达式的结果为0或1,0代表逻辑 false ,1代表逻辑 true 。
注意:
//有个优先级问题
int a = 10,b = 20;
cout << (a == b) << endl; // cout << a == b << endl; 是错误的,必须在比较运算表达式外括一个括号
3.4逻辑表达式
主要有与(&&)、或(||)、非(!)三种。
4.程序流程结构
4.1选择结构
4.1.1 if语句
1.单行 if 语句,语法如下:
if (条件)
{
要执行的语句
}
注意的是 if(条件)
后面不要加分号,否则不管条件满足与否都会执行后面语句。
2.多行 if 语句,语法如下:
if (条件)
{
条件满足下要执行的语句
}
else
{
条件不满足要执行的语句
}
3.多条件的 if 语句,语法如下:
if (条件1)
{
条件1满足要执行的语句
}
else if(条件2)
{
条件1不满足,但条件2满足要执行的语句
}
else if(条件3)
{
条件1不满足、条件2不满足,但条件3满足要执行的语句
}
else
{
以上条件都不满足要执行的语句
}
4.此外 if 语句还支持嵌套,让条件判断更加精细
4.1.2三目运算符
语法为:表达式1 ? 表达式2 : 表达式3;
如果表达式1的值为真,执行表达式2,并返回表达式2的结果;如果表达式1的值为假,执行表达式3,并返回表达式3的结果。在C++中三目运算符返回的是变量,可以继续赋值,举例如下:
int a = 10;
int b = 20;
int c = 0;
c = (a > b ? a : b); //相当于c = b
(a > b ? a : b) = 100; //在C++中三目运算符返回的是变量,可以继续赋值,执行后a的值为10,b的值为100
通过上面例子可以知道,三目运算符表达式既可以作左值也可以作右值。
4.1.3 switch 语句
语法如下:
switch (表达式)
{
case 1:执行语句1;break;
case 2:执行语句2;break;
……
default:执行语句;break;
}
case 里面如果不写 break 程序会一直向下执行
与 if 的区别:
- switch 的缺点,判断的时候表达式只能是整型或者字符型,不可以是一个区间
- switch 的优点,结构清晰,执行效率高,尤其对比多条件的 if 语句
4.2循环结构
4.2.1 while 循环语句
语法:while (循环条件) {循环语句}
只要循环条件为真,就一直执行循环内的语句,要检查是否有跳出循环的出口,除非有的时候我们确实需要死循环。
做一个猜数字的案例,随机生成一个1到100的随机数,让玩家去猜,每次猜系统要提示玩家大了小了,直到玩家猜对为止,游戏结束。
生成一个1到100的随机数,代码为:
#include <ctime> //生成随机数必须要包含的头文件
int main()
{
//添加随机数种子,利用系统当前时间生成随机数
srand((unsigned int)time(NULL));
//生成1到100的随机数
int num = rand() % 100 + 1;
}
4.2.2 do while 循环语句
语法:do {循环语句} while(循环条件);
与 while 循环语句不同的是 do while 至少保证代码执行一次。
可以写一个水仙花数案例,找出所有的水仙花数,水仙花数是指一个三位数,它各位数字的三次幂之和等于它本身的数。
4.2.3 for 循环语句
语法:for (起始表达式;条件表达式;末尾循环体) {循环语句}
注意 for 循环中的表达式要用分号分隔
4.2.4循环嵌套
可以尝试打印9*9乘法表
4.3跳转语句
4.3.1 break 语句
用于
- switch 语句中,中止 case 跳出 switch
- 循环语句中,跳出当前循环
- 嵌套循环中,跳出内层循环
4.3.2 continue 语句
用于在循环语句中,跳过本次循环中余下的未执行语句,继续执行下一次循环。例如可以充当一个筛选条件,打印0到100之间所有的奇数。
4.3.3 goto 语句
可以无条件的跳转代码,语法是 goto 标记;
这个标记一般用大写的单词来定义,例如:FLAG。在所需要跳转到地方写一个FLAG:
然后当程序执行到 goto 语句的时候就能实现跳转。不过由于 goto 语句过于强大,不推荐使用,容易造成程序流程混乱。示例如下:
int main()
{
cout << "1" << endl;
goto FLAG;
cout << "2" << endl;
cout << "3" << endl;
cout << "4" << endl;
FLAG:
cout << "5" << endl;
//程序运行结果是只打印1和5
}
5.数组
5.1概述
数组两个特点
- 数组中的每个元素都是相同的数据类型
- 数组存放在连续内存中
5.2一维数组
5.2.1一维数组定义方式
一维数组定义的三种方式
1.数据类型 数组名[数组长度];
2.数据类型 数组名[数组长度] = {值1,值2……};
定义的时候赋初值,如果初始化的时候没有全部填写完会用0来填充剩余的数据比如:
1
int arr[5] = {10,20,30}; //那么arr[3]和arr[4]就都为0了
3.数据类型 数组名[] = {值1,值2……};
初始化几个值,数组长度就为几,不允许不赋值如:int arr[];
错误!
需要注意的是一旦采用第一种方式,后面就不能群体赋值了
5.2.2一维数组数组名
一维数组名称的用途:
- 可以统计整个数组在内存中的长度
sizeof(数组名);
,返回结果为字节数。如果要看每个元素占用的大小代码为sizeof(数组名[0]);
如果要看元素个数两个相除就可以。 - 可以获取数组在内存中的首地址,示例如下:
cout << "数组首地址为:" << arr << endl;
cout << "数组中第一个元素的地址为:" << &arr[0] << endl; //取地址符号很重要
cout << "数组首地址为:" << (int)arr << endl; //把十六进制的地址转化成十进制
数组名一旦定义就成为一个常量,不能再对其进行赋值操作。
5.2.3冒泡排序
冒泡排序是一个比较重要的排序算法,下面为升序的冒泡排序代码:
//对一个已知的数组 arr 做冒泡排序
int i,j,temp;
int len;
len = sizeof(arr)/sizeof(arr[0]);
//开始冒泡排序,i表示轮数,从第0轮开始一直到第(len-2)轮,第i轮需要进行(len-i-1)次比较
for(i=0;i<len-1;i++)
{
for(j=0;j<len-i-1;j++)
{
if(arr[j] > arr[j+1])
{
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
5.3二维数组
5.3.1二维数组定义方式
二维数组定义的四种方式
数据类型 数组名[行数][列数];
数据类型 数组名[行数][列数]={{数据1,数据2},{数据3,数据4}};
数据类型 数组名[行数][列数]={数据1,数据2,数据3,数据4};
数据类型 数组名[][列数]={数据1,数据2,数据3,数据4};
需要注意的是一旦采用第一种方式,后面就不能群体赋值了
这里比较推荐第二种定义方式,直观简单。
5.3.2二维数组数组名
一维数组名称的用途:
-
查看二维数组所占内存空间。可以统计整个数组在内存中的大小
sizeof(数组名);
,返回结果为字节数。如果要看每一行占用的大小代码为sizeof(数组名[0]);
,如果要看每一个元素占用内存的大小代码为sizeof(数组名[0][0])
。计算二维数组行数代码为
sizeof(数组名)/sizeof(数组名[0]);
,计算二维数组元素个数代码为sizeof(数组名)/sizeof(数组名[0][0]);
-
获取二维数组首地址。示例如下:
cout << "数组首地址为:" << arr << endl;
cout << "数组第一行的首地址为:" << arr[0] << endl;
cout << "数组第一个元素的地址为:" << &arr[0][0] << endl;
cout << "数组首地址为:" << (int)arr << endl; //把十六进制的地址转化成十进制
5.3.3二维数组应用案例
成绩统计,每个人有多门课程成绩,需要用到二维数组
在存储人名的时候可以用一个字符串数组,如下:
#include <string> //用字符串型的数据 string 必须加的头文件
string names[3] = {"张三","李四","王五"};
6.函数
6.1概述
作用:将一段经常使用的代码封装起来,减少重复性代码。
6.2函数的定义
语法如下:
返回值类型 函数名 (参数列表)
{
函数体语句;
return表达式;
}
6.3函数的调用
语法为:函数名 (参数);
6.4值传递
值传递的时候,形参发生任何改变都不会影响实参。可以从内存分配的角度来理解。
6.5函数常见样式
常见有四种类型:
- 无参无返
- 无参有返
- 有参无返
- 有参有返
6.6函数的声明
一般情况下,自定义的函数要写在main函数的前面,如果自定义函数写在main函数之后,需要用到函数声明,语法为:
返回值类型 函数名 (参数列表);
而且声明可以写多次,但是定义只能定义一次。
6.7函数的分文件编写
步骤为:
- 创建.h后缀的头文件
- 创建.cpp后缀的源文件
- 在头文件中写函数的声明
- 在源文件中写函数的定义
注意:在上述的源文件中,开头要写 #include "头文件名.h"
这个就代表了头文件和源文件它俩之间是配套的(即有关联的) ;在上述的头文件中,要包含源文件用到的一些标准库,比如源文件中用到cout、cin,那么头文件开头就要写 #include<iostream>
和 using namespace std;
最后,在main函数所在的源文件上方写 #include "头文件名.h"
。
7.指针
7.1指针的基本概念
指针的作用:可以通过指针间接访问内存
- 内存编号是从0开始记录的,一般用十六进制数字表示
- 可以利用指针变量保存地址,即指针变量存放的是一个地址
7.2指针变量的定义和使用
指针变量定义的语法为:数据类型 *变量名;
下面为示例:
int a = 10;
int *p; //指针的定义
p = &a; //指针变量赋值,也可以定义时一步搞定 int *p = &a;
cout << "变量a的地址为:" << &a << endl;
cout << "指针p的值为:" << p << endl; //两个应该是同样的值
*p = 20; //指针的解引用,通过*p来修改a的值,利用指针变量将a的值修改为20
cout << "a的值为:" << a << endl;
cout << "*p = " << *p << endl; //通过解引用的方式对变量进行修改和读取
7.3指针所占内存空间
在32位操作系统下,指针都是占4个字节大小空间,不管是什么数据类型;在64位操作系统下,指针都是占8个字节大小空间,不管是什么数据类型。
7.4空指针和野指针
空指针作用:用于给指针变量进行初始化,空指针是不可以进行访问的,空指针NULL是0值,而0到255之间的内存编号是系统占用的,因此不可以访问。
野指针:指向非法的内存空间,示例如下:
int *p = (int *)0x1100; //将一个十六进制数强转为地址
cout << *p << endl; //访问野指针报错
空指针和野指针都不是我们申请的内存空间,因此不要去访问。
7.5 const 修饰指针
分为以下三种:
-
const修饰指针 —常量指针
定义为:
const int *p = &a;
特点是:指针的指向可以修改,但是指针指向的值不可以改。
*p = 20;
错误。p = &b;
正确。 -
const修饰常量 —指针常量
定义为:
int * const p = &a;
特点是:指针的指向不可以修改,但是指针指向的值可以改。
*p = 20;
正确。p = &b;
错误。 -
const既修饰常量又修饰指针
定义为:
const int * const p = &a;
特点是:指针的指向和其指向的值都不可以修改。
*p = 20;
和p = &b;
均错误。
7.6指针和数组
利用指针来访问数组中的元素,下面为用指针遍历数组的示例代码:
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int i;
int *p = arr;
for(i=0;i<10;i++)
{
cout << *p << endl;
p++; //每次往后偏移4个字节地址
}
7.7指针和函数
利用指针作函数参数,可以修改实参的值,示例代码如下:
void swap1(int a,int b) //值传递不会改变实参
{
int temp = a;
a = b;
b = temp;
}
void swap2(int *p1,int *p2) //地址传递会改变实参
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main()
{
int a = 10 , b = 20;
swap1(a,b);
cout << "a和b的值分别为:" << a << b << endl; //这一步输出a和b的值并没有发生交换
swap2(&a,&b);
cout << "a和b的值分别为:" << a << b << endl; //这一步a和b的值就真的发生了交换
}
这就是值传递与地址传递的不同之处。
7.8指针、数组、函数
做一个综合案例,封装一个函数,利用冒泡排序实现对一个数组的升序排序。数组为 int arr[10] = {4,3,6,9,1,2,10,8,7,5};
void bubbleSort(int *arr,int len)
{
int i,j,temp;
for(i=0;i<len-1;i++)
{
for(j=0;j<len-i-1;j++)
{
if(arr[j] > arr[j+1])
{
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
void printarr(int *arr,int len)
{
for(int i=0;i<len;i++)
{
cout << arr[i] << endl;
}
}
int main()
{
int arr[10] = {4,3,6,9,1,2,10,8,7,5};
int len = sizeof(arr)/sizeof(arr[0]);
bubbleSort(arr,len);
printarr(arr,len);
system("pause");
return 0;
}
通过这个案例可以知道,当我们要向一个函数传递一个数组的时候,往往要用指针去接收它,而且为了方便一般还要传入数组的长度。
8.结构体
8.1结构体基本概念
结构体属于用户自定义的数据类型,允许用户存储不同的数据类型。
8.2结构体的定义与使用
语法:struct 结构体名 {结构体成员列表};
通过结构体创建变量的方式有三种:
struct 结构体名 变量名;
struct 结构体名 变量名 = {成员1的初值,成员2的初值……};
- 定义结构体时顺便创建变量
struct student
{
string name;
int age;
int score;
}s3; //第三种创建方式,定义结构体时顺便创建变量,之后像第一种一样利用.来对其属性赋值
int main()
{
//第一种创建方式,之后想要赋值就要用.来访问其属性
struct student s1; //这里可以省去struct,但是在结构体定义时struct不能省
s1.name = "张三";
s1.age = 18;
s1.score = 90;
//第二种创建方式,创建时就赋初值
struct student s2 = {"李四",19,95};
}
8.3结构体数组
语法:struct 结构体名 数组名[元素个数] = {{},{}……};
结构体数组与其他数组一样,可以在创建的时候赋初值,也可以只创建,之后用.号来赋属性的值。
8.4结构体指针
利用操作符->
可以通过结构体指针访问结构体属性。
struct student s = {"张三",18,90};
struct student *p = &s;
p->name = "李四"; //与其他数据类型的指针不同
8.5结构体嵌套结构体
在结构体中可以定义另一个结构体作为成员,形成结构体嵌套结构体。
struct teacher
{
string name;
int age;
struct student stu;
};
int main()
{
struct teacher t;
t.name = "老王";
t.age = "30";
t.stu.name = "小王";
t.stu.age = 18;
t.stu.score = 90; //利用多个.号访问作为成员的结构体的属性
}
8.6结构体作函数参数
将结构体作为参数向函数中传递,传递方式有两种:
- 值传递
- 地址传递
比如说,向子函数传一个学生的结构体,要求子函数打印学生的信息,代码如下:
void printstu1(struct student s)
{
//s.name = "李四";回到main函数在,实参值不变,还为原来
cout << "学生姓名:" << s.name << "成绩:" << s.score << endl;
}
void printstu2(struct stduent *p)
{
//p->name = "李四";回到main函数中,实参的值会真的被修改
cout << "学生姓名:" << p->name << "成绩:" << p->score << endl;
}
int main()
{
struct student s = {"张三",18,90};
printstu1(s); //值传递
printstu2(&s); //地址传递
}
值传递和地址传递的不同在于:值传递不能改变实参的值,而地址传递可以。
8.7结构体中 const 的使用场景
作用:用 const 来限定只读操作,防止误修改
应用场景的产生:在向子函数传递一个结构体时,如果采用值传递,那么会在子函数中复制出一个新的副本,当结构体十分庞大时会占用比较多的内存资源,所以我们采用地址传递(只传入地址4个字节节省内存空间),但是地址传递可以修改实参的值,容易造成误修改。此时就有了使用 const 的必要 ,加入 const 之后一旦有修改的操作就会报错,可以防止误操作。
void printstu(const struct student *p)
{
//p->age = 20; 加了const后这句代码就会报错,从而避免了实参值的误修改
cout << p->name << p->age << p->score << endl;
}
int main()
{
struct student s = {"张三",18,90};
printstu(&s);
}