C++基础入门

前言

​ 优秀的C++ 书籍把精力集中于与面向对象模型相关的课题上(如类的设计),而不是专注于基本的C技巧,这样做是对的。但C++ 是建立在C 基础之上的,掌握C 的基本技巧依然非常重要。

C++基础语法

1 初识C++

B语言

​ 1969年,美国贝尔实验室的Ken Thompson。以BCPL语言为基础,设计出很简单且很接近硬件的B语言(取BCPL的首字母)。并且他用 B语言 写了第一个 UNIX 操作系统。

C语言

C语言之父,UNIX之父 丹尼斯·里奇

​ C语言的发明原因很简单, 就是C语言之父 丹尼斯·里奇 和同事在开发Unix系统时需要更好的语言工具, 当时没有“高级”语言来更多地控制所有涵盖操作系统的数据。所以在开发Unix操作系统的同时,就发明了C语言;

​ 1969-1973年在美国电话电报公司 贝尔实验室开始了C语言的最初研发。根据C语言的发明者丹尼斯·里奇说,C 语言最重要的研发时期是在1972年

C++ 语言

C++ 语言是在C语言的基础上扩充而来的, 同样也是在贝尔实验室诞生的;

1982年, 本贾尼·斯特劳斯特卢普 发明C++语言;

C++ 之父

这里插一张C++ 语言之父的肖像图在这镇楼

image-20191231094523665

2 数据类型

整型

名称 描述 大小(字节)
short 短整型 2
int 整型 4
long 长整型 win 4字节 / linux 8字节
long long 长长整型 8

整型大小: short < int <= long < long long

注:

​ 在C++中,八进制数以0开头,如012转换成十进制是10;十六进制数以0x开头,如0x64是十进制的100。

浮点型

名称 描述 大小(字节)
float 单精度浮点型 4
double 双精度浮点型 8

科学计数法 float f1 = 3e2; // 3* 10^2 = 300 ; float f2 = 3e-2; // 3* 0.1^2 = 0.03;

字符型

名称 描述 大小(字节)
char 字符型 1

​ 字符要用单引号,如 char a = 'a' ;
注:

​ C/C++ 中的字符变量只占用1个字节; 字符型变量并不是把字符本身放到内存中存储, 而是把对应的ASCII编码放到存储单元;

转义字符

\n 换行符

\t 水平制表Tab

\ 代表一个反斜杠”"

**字符串型 **

在C++ 中,有两种风格的字符串写法:

char str[] = "hello word";		//C风格写法 
string str = "hello word";		//C++风格写法   

注意:

​ 这里的两种写法和java非常相似,但又有不同, 在java中,char str [] 代表数组, 只能跟{ }, 而java的字符串类型是String,C++是string。

布尔类型

名称 描述 大小(字节)
bool 布尔型 1

小结

C++ 中的基本类型如下:

short		短整型
int			整型
long		长整型
long long	长长整型
char		字符型
bool		布尔型

数据类型所占的内存空间大小不用刻意去记, c++中可以使用 sizeof 关键字计算数据类型所占内存大小:

sizeof(数据类型/变量)

3 运算符

3.1算数运算符

运算符 术语 示例 结果
+ 正号 +3 3
- 负号 -3 -3
+
-
*
/
% 取余 10%3 1
++i 前置递增 a=2;b=++a; a=3;b=3;
i++ 后置递增 a=2;b=a++; a=3;b=2;
–i 前置递减 a=2;b=–a; a=1;b=1;
i– 后置递减 a=2;b=a–; a=1;b=2;

3.2赋值运算符

运算符 术语 示例 结果
= 赋值
+= 加等于
-= 减等于
*= 乘等于
/= 除等于
%= 模等于

3.3比较运算符

运算符 术语 示例 结果
== 等于
!= 不等于
> 大于
>= 大于等于
< 小于
<= 小于等于

3.4逻辑运算符

运算符 术语 示例 结果
||
&&
!

4 程序流程结构

C/C++ 中支持3种基本的程序运行结构: 顺序结构, 选择结构, 循环结构

选择结构

if-else 语句

int num = 600;
if(num > 600){
    cout << "数量大于600" <<endl;
}else if(num > 300){
    
}else{
    
}

switch 语句

int num = 600;
switch(num){
    case 100:
        cout << "xxx" <<endl;
        break;
    case 600:
        cout << "yyy" <<endl;
        break;
    default:
        cout << "zzz" <<endl;
}

三目运算符

由 ? 和 : 组成的条件判断表达式,问号前面是逻辑表达式,如果为true,返回结果一,否则返回结果二;

int num = 600;
int score = num>600 ? 600 : 0;

循环结构

循环语句有3个,和java没有区别,循环语句配合跳转关键字continue, break 使用;

break 用于跳出 选择结构或者循环结构

while语句

int i = 0;
while(i<10){
   i++; 
}

while 括号中的表达式成立,才会执行语句;

do while语句

int i = 0;
do{
  i++;
}while(i<10)

do while 语句是先执行do 代码块中的语句,再判断while语句;至少执行1遍;

for 循环

for(int i = 0;i<100;i++){
  
}

for语句有3个表达式

表达式一:初始化语句,只执行一次;

表达式二:循环判断语句,条件满足,执行循环体,否则循环结束;

表达式三:每次循环结束后需要执行的表达式;

如果表达式二省略,即不判断循环条件,那么这是个死循环:

for(;;){
  
}

死循环中如果遇到 break,一样可以跳出循环体。

continue 和 break

continue 和 break 在循环体中控制流程:

  • continue:结束本次循环,开始下一次循环;
  • break:结束整个循环体;

goto 灵活跳转

goto FLAG;
	 cout << "xxx" <<endl;
FLAG:
	 cout << "zzz" <<endl;

注:

​ 因为 goto 控制逻辑过于灵活,如果在程序中大量使用,代码将会难以调试和阅读,因此在程序中尽量减少使用。

5 数组

一维数组

int array[3] = {1,2,3};

二维数组

二维数组可以表示一个矩阵;

数据类型 数组名 [行数] [列数] ;

//定义方式一:
int array[2][3];  	//行数,列数
[0][0] = 1;			//第一行,第一列
[0][1] = 2;			//第一行,第二列
[0][2] = 3;
[1][0] = 4;
[1][1] = 5;
[1][2] = 6;

//定义方式二:(也是最常用的)
int array2[2][3]={
    {1,2,3},
    {4,5,6}
}; 

for(int i=0;i<2;i++){
    for(int j=0;j<3;j++){
    	cout<< array[i][j] <<endl;
	}
}

6 函数

c++中的函数和java 还是有一定差别的.

函数的可以分为声明定义两步来写, 这和变量的声明初始化是一样的;

如果函数的声明和调用在同一个源文件中, 那么函数的声明必须在调用之前写完,否则程序报错.

这一点限制的还是比较死的; 不同于java, java中的函数编定义位置和调用位置没有限制的这么死;

当然如果在C++中,函数的声明/定义和调用是分文件编写的, 那么在源文件中include头文件,就可以直接调用了.

小结:

​ C++中函数可以分为声明和定义,在java的普通类中,没有区分声明和定义, 只有直接编写;

​ C++中函数的声明像极了java中的抽象方法, 只有声明,没有具体实现;

​ 在同一个文件中,如果函数是先声明再定义, 那么声明必须在调用之前编写, 定义的位置没有限制;

​ 如果函数没有声明,直接定义, 那么定义必须在调用之前编写;

​ java中函数的编写要比C++灵活很多;

函数的声明

int switch0(int num1,int num2);

函数的定义

int switch0(int num1,int num2){
    int temp = num1;
    num1 = num2;
    num2 = temp;
}

函数的分文件编写

函数的分文件编写分2步走:

  1. 创建.h后缀名的头文件,编写函数的声明;
  2. 创建.cpp后缀名的源文件, 编写函数的定义;

swap.h 头文件:

//在头文件中声明函数
int switch0(int num1,int num2);

swap.cpp 源文件:

//引入头文件
#include "swap.h"

int switch0(int num1,int num2){
   int temp = num1;
   num1 = num2;
   num2 = temp;
}

参数传递方式

函数参数传递的方式有2种:

  1. 值传递
  2. 地址传递

值传递: 形参的值发生改变, 实参不会改变;

地址传递: 形参的值发生改变, 实参也会改变;

7 指针

每个变量都有一个内存地址,使用 & 变量名 可以读取到变量的内存地址:

int a = 10;
cout << "var1 变量的地址:"<< &a << endl;;

基本概念

指针本质上是一个变量,保存的是一个地址;

指针的作用: 可以通过指针直接或间接的访问内存数据;

  • 内存地址是从0开始记录的, 一般用十六进制数字表示;
  • 可以利用指针变量保存地址;

指针变量的定义和使用

指针定义的语法: 数据类型 指针变量名* ;

变量取址的语法: &变量名;

int a = 10;		//定义变量
int* p;			//定义指针
p = &a;			//变量a取址并让指针p记录该地址

使用指针:

​ 通过解引用的方式来找到指针所指向的内存数据

解引用语法: *** 指针变量名**;

int b = *p; 	//指针解引用

注:

​ int* p 中,声明的int 数据类型,不是说明指针 p 是 int 类型,而是指针 p 指向的内存地址存放的数据是int 类型。

​ 指针变量占用固定的空间大小。

指针所占内存空间

32位操作系统下, 指针占用 4个字节;

64位操作系统下, 指针占用 8个字节;

空指针和野指针

空指针: 指针变量指向内存中编号为0的空间.

注意: 空指针指向的内存是不可以访问的!

int* p2 = NULL;		//空指针

野指针: 指针变量指向非法的内存空间;

int* p3 = (int*)0x1100;	//野指针

注意:

​ 空指针和野指针都不是自己分配的内存,所以都不能访问!

const 修饰指针

const 修饰指针的三种情况:

常量指针

const 直接修饰指针: const int* p ,叫常量指针;

特点:

指针的指向的值不可以改, 指针的指向可以改;

int a = 10;
int b = 20;

const int* p = &a;
*p =20; //错误写法: 指针指向的值不可以改;
p = &b; //正确写法

指针常量

const 直接修饰常量: int* const p ,叫指针常量;

特点:

指针的指向不可以改, 指针的指向的值可以改;

int a = 10;
int b = 20;

int * const p = &a;
*p = 20; //正确写法
p = &b; //错误写法: 指针的指向不可以修改;

全常量指针

指针的指向和指向的值都不可以改!

int a = 10;
int b = 20;

const int * const p = &a;
*p =20; //错误写法
p = &b; //错误写法

技巧:

  1. const 翻译为常量, int* 翻译为指针,所以 const int* p 叫常量指针; int* const p 叫指针常量;

  2. const 修饰谁,谁就不能改;
    const int* p 修饰的是*, 那么 *p (指针的解引用的值)就不能改;

    int* const p修饰的是p, 那么 p (指针的引用)就不能改;

指针运算

int a = 10;
int* p = &a;

指针与目标变量是密切相关的,

不能给指针变量赋值自定义值(如 int* p = 0x1000),给指针变量赋的值必须是合法的内存地址。

指针变量只支持加法和减法,不支持乘法、除法和取余运行;

指针和数组

我们知道可以用数组下标来访问数组元素;

利用指针来访问数组元素;

int arr[10] = {1,2,3,4,5,6,7,8,9,10};	//初始化数组
int a = arr[0];		//使用数组下标访问数组元素

int * p = arr;		//数组名就是数组首个元素的地址
int b = *p;			//解引用

for(int i =0;i<10;i++){
    cout << arr[i] <<endl;
    cout << *p <<endl;	//指针解引用,找到指向的内存数据;
    p++;				//指针后移;
}

指针和函数

//值传递
int a =10;
int b =20;

void swap1(int a,int b){
    int temp = a;
    a = b;
    b = temp;
}
swap1(a,b);
cout << a <<endl;
cout << b <<endl; //结果a=10, b=20;

//地址传递
void swap2(int* p1,int* p2){
    int temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}
swap2(&a,&b);
cout << a <<endl;
cout << b <<endl; //结果a=20, b=10;

8 结构体

结构体的定义和使用

结构体是自定义的数据类型 ;

使用 struct 关键字来定义结构体.

struct Student{
	int id;
	string name;
	int age;
};

注:

​ 最后的分号不可以省略,定义结构体时,结构体内的属性不可以赋值(如:int age = 18),否则非法报错;定义结构体不会分配内存,创建结构体变量才会分配内存。

2种方式创建结构体变量

//方式一:
Student stu = {1,"wyc",18};

//方式二:
Student stu ;
stu.id = 1;
stu.name = "wyc";
stu.age = 24;

cout << "wyc: " << stu.name << "" << endl;

​ 使用方式一有一个缺点,那就是结构体成员的初始化顺序必须和声明时保持一致,如果一个成员不赋值,那么后面的所有成员都不能赋值;

注:

​ 定义结构体时, struct 关键字不可以省略;

​ 创建结构体时, struct 关键字可以省略;

​ 结构体中可以声明成员函数,但很少这么做,一般只会声明成员变量;

​ 结构体成员的访问权限默认是public,类成员默认是private,这一点是比较大的区别。

​ 结构体成员通常不会额外加访问修饰符,使用默认的public即可;

结构体数组

Student stus[3]{
	{1,"wyc",18},
	{2,"www",19},
	{3,"jack",25}
};

结构体指针

//1.创建结构体变量
Student stu = {1,"wyc",18};
//2.通过指针指向结构体变量
Student* p = &stu;
//3.通过指针访问结构体变量中的数据 
int id = p->id;
string name = p->name;
int age = (*p).age;

注:

​ 通过指针访问结构体变量中的数据有两种方式:p->name(*p).name ;它们都等价于结构体变量.属性 stu.name

结构体嵌套结构体

struct Student{
	int id;
	string name;
	int age;
};

struct Teacher{
	int id;
	string name;
	int age;
    Student stu;
};

int main(){
    Teacher teacher;
    teacher.id = 001;
    teacher.name = "jack";
    teacher.age = 39;
    
    teacher.stu.id=020;
    teacher.stu.name="wyc";
    teacher.stu.age = 24;
}

结构体做函数参数

函数的参数传递方式有2种: 值传递 和 地址传递;

Student stu = {01,"jack",18};

//值传递
void printStudent01(Student stu){
    stu.age = 28;
    cout << stu.age << endl;
}

//地址传递
void printStudent02(Student* stu){
    stu->age = 28;
    cout << stu->age << endl;
}

printStudent01(stu);
printStudent02(&stu);

cout << stu.age << endl;	

​ 结果同上面的基本数据类型一样, 值传递时, 作为形参的结构体属性发生变化, 实参不会变化;

而地址传递, 形参改变, 实参也会改变;

总结:

​ 如果你不想改变主函数中的数据,用值传递; 反之用地址传递;

联合体

枚举

C++核心编程

1. 内存分区模型

  1. 代码存储区 : 存放函数体的二进制代码。
  2. 全局/静态存储区: 全局变量和静态变量的存储是放在一块的. 局部静态变量也会存储在全局/静态区;
  3. 常量存储区 : 常量存储区 和 全局/静态存储区 的地址很接近;
  4. 栈区(stack): 由编译器自动分配释放 ,存放函数的形参,局部变量等。其操作方式类似于数据结构中的栈。
  5. 堆区(heap): 由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

内存分区的意义:

​ 不同区域存放的数据,赋予不同的生命周期, 方便我们灵活编程;

//查看各变量的内存地址:
int a = 10;
int b = 10;
const int c = 10;
const int d = 10;

static int i = 10;
static int j = 10;
const static int m = 10;
const static int n = 10;

string str1 = "www";
string str2 = "www";
const string str3 = "www";
const string str4 = "www";

void test(int x){
	int e = 10;
	int f = 10;
	const int g = 10;
	const int h = 10;
}

int main(){
	test(3);
	system("pause");
	return 0;
}

image-20191231094817213

程序运行前

程序编译后, 生成exe的可执行程序, 未执行程序前分为两个区域

代码区:

​ 存放CPU执行的机器指令;

​ 代码区是共享的, 共享的目的是对于频繁被执行的程序, 只需要在内存中有一份代码即可;

​ 代码区是只读的, 防止程序意外的修改它的指令;

全局区:

​ 该区域的数据在程序结束后由操作系统释放;

程序运行后

栈区:

​ 由编译器自动分配和释放, 存放函数的参数值 , 局部变量等;

​ 注意: 栈区的数据在函数执行完毕后自动释放, 所以不要返回局部变量的地址 ;

栈区:

​ 由程序员分配和释放, 若不手动释放, 程序结束时由操作系统回收;

​ 在C++中使用 new 关键字开辟内存, 使用 delete关键字释放内存,

void fun(){
    //在栈中分配内存,函数执行完,自动释放内存
	int a = 10;		
    //在堆中分配内存, 返回地址,所以用指针接收; 该内存不会自动释放,需要手动释放;  
  	int* b = new int(10);	
    //释放内存;
    delete b;
}

new 和 delete 关键字

C++中使用 new 关键字在堆区分配内存;

堆区开辟的内存, 都需要手动开辟, 手动释放; 使用 delete 关键字释放内存;

语法 : new 数据类型

new 开辟内存后, 返回该内存的地址,所以要用指针接收;

int* a = new int(10);	//在堆中分配整型数据,值为10
delete a;		//释放内存用delete;

int* arr = new int [10];	//在堆中开辟数组
delete[] arr;		//释放数组要用delete[];

2. 引用

引用的基本使用

作用: 给变量起别名

语法: 数据类型 &别名 = 原名

int a = 10;
//创建引用
int &b = a;
b = 20;

cout << "a:" << a << endl;
cout << "b:" << b << endl;

引用的注意事项

  • 引用必须初始化.
  • 引用在初始化后, 不可以再改变.
int a = 10;
int b = 10;

//int &c;  		语法错误,引用必须初始化;
int &c = a;		//引用一旦初始化,就不可以更改;
c = b;			//这是赋值操作,不是更改引用;

引用做函数参数

作用: 函数传参时, 可以利用引用让形参修饰实参.

优点: 可以简化指针修改实参.

//值传递
void swap01(int a,int b) {

	int temp =a;
	a = b;
	b = temp;
}
//地址传递
void swap02(int* a, int* b) {

	int temp = *a;
	*a = *b;
	*b = temp;
}
//引用传递
void swap03(int &a, int &b) {

	int temp = a;
	a = b;
	b = temp;
}

int main() {
	int a = 10;
	int b = 10;
	swap01(a,b);
	swap02(&a, &b);
	swap03(a, b);
	return 0;
}

总结:

​ 通过引用参数产生的效果和地址传递是一样的, 引用语法更简单些;

引用做函数返回值

引用是可以作为函数的返回值存在的;

注意: 不要返回局部变量的引用

用法: 函数返回引用时, 给函数赋值, 也就是说函数调用可以作为左值;

//返回局部变量的引用
int& fun1(){
    int a = 10;
    return a;
}
//返回静态变量的引用
int& fun2(){
    static int a = 10;
    return a;
}

int main(){
    
    //int &ref1 = fun1();  //不要返回局部变量的引用(指针)!因为函数调用完后内存释放;
    int &ref2 = fun2();  //静态变量的引用地址不会释放,可以返回
    
    //这个写法就是本节说的: 函数返回值是引用,可以作为左值;相当于给函数返回的变量引用赋值;
    fun2() = 1000;		 
    return 0;
}

引用的本质

在C++中引用的本质是一个指针常量.

int main(){
    int a = 10;
    int& ref= a; //引用 (本质是一个指针常量) 相当于  int* const ref = &a;
	ref = 30;  //修改引用的值, a 和 ref 的值都发生改变;	相当于 *ref = 30;	
    return 0;
}

结论:

​ 引用在C++中是一个语法糖.

​ 在C++中推荐使用引用, 语法简洁方便;

​ 引用的本质是一个指针常量, 指针常量的指针指向不可以改变,所以引用一旦初始化,就不可以更改;

​ 引用所做的指针操作都是编译器帮我们做了;

常量引用

引用的本质是一个指针常量, 那么常量引用就是一个全常量指针;

// int& ref = 10;     语法错误:引用本身需要一个合法的内存空间;
//加上const,语法就对了,这是一个常量引用;相当于 int temp = 10; const int& ref2 = temp;
const int& ref2 = 10;	//相当于 const int* const ref2 = 10;

3. 函数高级

函数的参数默认参数

语法:

  1. 如果函数参数的某个位置有默认值, 那么这个位置往后,从左到右, 必须都要有默认值 ;
  2. 如果函数声明有默认值, 函数实现的时候就不能有默认值;
    函数声明没有默认值, 函数实现有默认值也是可以的, 但同样不能同时出现默认值的情况;
//如果函数参数的某个位置有默认值, 那么这个位置往后,从左到右, 必须都要有默认值; 否则语法报错;
int func01(int a, int b = 10,int c =20);	

//函数声明中参数有默认值,函数实现则不能有默认值;语法不会报错,但是运行会报错;
int func01(int a, int b,int c) {	
	return a + b + c;
}

int main(){
    
    int a = func01(10);		//返回值是 10+10+20=40;
    int b = func01(10,20);		//返回值是 10+20+20=50;
    int b = func01(10,20,30);		//返回值是 10+20+30=60;
}

说明:

  1. 不能同时出现默认值的原因很简单, 函数的声明和实现都有参数默认值, 那么就是一种冲突, 编译器不知道应该以哪个为准,所以不允许这样操作;
  2. 函数声明有参数默认值, 函数实现就不应该有了; 反之,函数实现有参数默认值, 函数声明就不应该有了;

函数的占位参数

C++函数的形参列表中可以有占位参数, 用来做占位, 调用函数时必须填补该位置.

语法: 返回值类型 函数名 (数据类型){...}

//函数占位参数
void func01(int a, int) {	
	
}
//占位参数也可以有默认值
void func02(int a, int = 10) {	
	
}

int main(){
    func01(10,10);	//函数占位参数必须填补;
	func02(10);		//有参数默认值则可以不填;
}

说明:

​ 现阶段函数的占位参数存在的意义不大, 后面会用到该技术, 现在只做了解;

函数重载

函数重载的满足条件:

  • 同一作用域下;
  • 函数名称相同, 参数类型或个数或顺序 不同;

注意:

​ 函数的返回值不作为重载的条件;

int func();
int func(int a);
int func(int a,int b);		//个数不同
int func(int a,double b);	//类型不同
int func(double a,int b);	//顺序不同

double func(double a,int b); //在同一作用域下,语法错误;

函数重载注意事项

  1. 引用作为函数重载的条件;
  2. 函数重载遇到默认参数的情况;
int func(int& a);
int func(const int& a);

int main(){
    int a = 10;
    const int b = 10;
    func(a);	//调用的是 func(int& a) 函数
    func(b);	//调用的是 func(const int& a) 函数
    func(10);	//调用的是 func(const int& a) 函数
}

说明:

​ 首先,引用作为函数重载的条件,是允许的;

在函数调用时, 传入变量,则走普通引用函数,; 传入常量,则走常量引用函数; 传入值,则也走常量引用函数, 因为

int& a = 10; 的语法是错误的, 所以只能走 func(const int& a) 函数;

int func(int a);
int func(int a,int b=10);

int main(){
    //func(10);		//语法错误:不知道调用哪一个函数;
    func(10,20);	//肯定调用int func(int a,int b=10);
}

4. 类和对象

C++面向对象的三大特性: ==封装, 继承,多态== ;

封装

封装属性和行为

访问权限

struct 和 class区别

对象的初始化和清理

C++对象模型和this指针

友元

运算符重载

继承

多态

5. 文件操作

STL

README

作者:米开

微信公众号:米开的网络私房菜

image-20200715095229689

版权声明:本文遵循**知识共享许可协议3.0(CC 协议)**: 署名-非商业性使用-相同方式共享 (by-nc-sa)

本文档很大部分内容来自《黑马程序员》提供的C++课程。

2019-12-22 王衍超 第一次修订


C++基础入门
http://jackpot-lang.online/2019/12/22/C_C++学习/C++基础入门/
作者
Jackpot
发布于
2019年12月22日
许可协议