C++
目录
文章目录
一 介绍
Bjarne Stroustrup(本贾尼·斯特劳斯特卢普,于1979年在分析Linux系统分布内核流量分析时,希望有一个更加模块化的工具,于是他为C语言增加了类的机制(面向对象),1983年完成了C++的第一个版本。
特点:
- C++完全兼容C语言。
- 支持面向对象的编程思想(抽象,封装,继承,多态)。
- 支持函数,运算符重载。
- 支持泛型编程,模板技术。
- 支持异常处理机制。
- 类型检查更严格。
二 第一个C++程
1.文件扩展名
.cpp .cc .C .cxx
2.头文件
#include
#include <stdio.h>//可以继续使用
#include //建议这样使用 里面删除了一些可能引起冲突的宏
3. using namespace std;//增加了名字空间
4. 输入输出函数
-
cout << 输出数据
-
cin >> 输入数据
-
不需要对变量取地址,也不需要占位符,因为他们可以自动识别数据类型。
-
scanf/printf/sscanf/sprintf 可以继续使用
注意:
scanf/printf是C标准库函数,cin/cout是C++标准库中的类对象,被定义在std名字空间中。
5. 编译器
g++ file.cpp -> a.out
大多数LInux系统都需要额外添加参数,ubuntu系统安装命令:
- sudo apt-get updata
- sudo apt-get install g++
gcc编译器也可以继续使用,需要额外增加参数, gcc -xc++ xx.cpp -lstdc++
6. 练习
1. Hello
#include <iostream>
#include <stdio.h>//可以继续使用
#include <cstdio>//建议这样使用 里面删除了一些可能引起冲突的宏
using namespace std;//增加了名字空间
int main(int argc,char* const argv[])
{
cout << "Hello World"<<endl;
return 0;
}
2.求最大,最小,平均
#include <iostream>
using namespace std;
int main(int argc,const char* argv[])
{
int n,x,max,min;
double s=0;
cin >> n;
cin>>x;
max=x;
min=x;
s = 1.0*x/n;
for(int i=1;i<n;i++)
{
cin >> x;
if(max < x)
max = x;
if(min > x)
min = x;
s+= 1.0*x/n;
}
cout <<"最大值: "<< max<<"最小值: "<<min<<"平均值: "<<s;
}
三 名字空间
1.为什么需要名字空间
在项目中函数名,全局变量,结构,联合,枚举,宏等,非常有可能命名冲突,而C++之父设计名字空间就是为了解决命名冲突,名字空间可以把这些命名划分到不同逻辑空间里,从而解决命名冲突
2.定义名字空间
namespace name{
定义变量
定义函数
定义类,结构,联合,枚举
}
- 空间名可以重复,会自动合并
3.使用名字空间
使用空间中的单个标识符, 空间名::标识符,安全
导入空间中所有的标识符,using namespace 空间名,方便依然有冲突的可能
4.名字空间的嵌套
定义: 名字空间的内部可以再定义名字空间
内层标识符可以与外层重名,内层会自动屏蔽外层的标识符。
namespace n1{
int x =10;
namespace n2{
int x = 20;
namespace n3:{
int x=30;
}
}
}
多层空间使用时需要逐层分解
n1::n2::n3::x
使用多层名字空间时为了精简长度可以取别名
namespace n123 =n1::n2:n3; 效果等同于 n3
5.匿名空间
默认定义全局的标识符归属到匿名空间中,可以直接使用域限定符::访问匿名空间中的标识符。
#include <iostream>
using namespace std;//增加了名字空间
int x =10;
int main(int argc,char* const argv[])
{
int x = 20;
cout << x <<endl; //局部 输出20
cout << ::x<<endl//全局(匿名空间) 输出10
return 0;
}
四. 数据类型
1.布尔类型
C++中有真正的布尔类型,不需要加头文件,bool true false 可以直接使用
2.结构(struct)联合(union)
- 不再需要typedef,在定义结构,联合变量时,可以省略 struct union关键字
- 成员可以是函数,成员函数可以直接访问成员变量,不需要 . ->
#include <iostream>
using namespace std;
struct Student //不需要typedef
{
char name[20];
char sex;
int age;
void func(void) //成员函数可以直接访问成员变量
{
cout<<"name: " <<name <<endl;
cout<<"sex: " <<sex<<endl;
cout<<"age: " << age <<endl;
}
};
int main(int argc,const char* argv[])
{
Student stu = {"小明",'w',22}; // struct 省略
stu.func();
}
-
有四个隐藏的成员函数(构造,析构,拷贝构造,赋值构造)
-
可以对成员的访问权限进行管理。 public/protected/private
-
结构还可以被继承
3. 枚举(enum)
- 不需要typedef,在定义枚举变量时可以省略enum 关键字
- 使用方法与C语言基本一致,不能与整形数据混用
#include <iostream>
using namespace std;
enum Key
{
up=183,dowm=184,right=185,left=186
};
int main(int argc,const char* argv[])
{
Key P = 182;//错误: 从类型‘int’到类型‘Key’的转换无效
}
4. void*
- 在c语言中void*可以与任意类型指针自动转换(万能指针)
void* p = NULL;
int* p1 = p;
p = p1;
- C++中void*不能再直接给其他类型指针变量赋值,只能强制类型转换后才能赋值,但其他类型指针可以直接给void *类型的指针赋值
void* p = NULL;
int* p1 = p; //error
int* p1 = (int*) p //正确
- C++为什么要修改 viod *
为了安全,另外C++可以自动识别类型,所以对万能指针的需求不再那么强烈。
5. 字符串
-
C++中把字符串封装成了string类,实现在string中,但已经被iostream直接包含,定义在std名字空间中。
-
常见的字符串操作不再需要函数,可以使用运算符:
- = strcpy
- == strcmp
- += strcat
-
计算字符串成员长度使用 size成员函数
-
也可以用与转换成C语言中的char*
* c_str() const char* * data() char*
五 函数
1. 函数重载
-
什么是函数重载
- C++中 在同一作用域下,形参列表不同的同名的函数构成重载关系且不会冲突。
- 调用函数时,编译器会根据实参的数据调用合适的函数
-
重载实现的机制
- C++代码在编译时 函数的参数类型会添加到函数名中,也就是说C++函数在编译时会经历换名的过程,借助这个方式实现了函数的重载。
- 由于C++和C函数的编译机制不同,所以C++代码不能调用C编译器所编译出的函数。
-
extern “C” { }
- 告诉C++编译器按照C语言的机制声 明函数,这样C++的代码就可以调用C编译器 编译出的函数了(C++目标文件与C的目标文件才能合并出可执行文件)
-
重载跟作用域
- 函数的重载关系一定发生在同一作用域下,不同作用下的同名函数构成的是隐藏关系
-
重载函数的调用
当调用重载函数时,编译器会根据实参的数据类型选择合适的重载函数,实参与形参匹配的情况有三种:
- 编译器找到实参与形参完全匹配的函数,编译器会生成调用指令
- 编译器找到多个匹配函数,但没有一个最佳的,编译器会产生二义错误。
- 绝大数情况下都编译器都能找到一个最佳的匹配函数,但如果没有,编译器就会进行类型提升,这样备选函数中就可能有多个可调用的版本,然后二义性错误就产生了。
- 编译器找不可调用的重载函数,会直接产生错误。
-
指针类型也会影响函数的重载
C++函数的参数如果是指针类型的,编译时就会在函数的末尾添加 Px。(x为指针的类型)
-
如果参数是指针或引用,是否加const也会影响函数的重载,PKx。
函数重载 注意:
函数重载是面向对象编程思想的多态(多种形态,根据实参情况对指令作出相应的反应)的体现,具体调用哪个版本的函数是在编译期间就确定了,所以这种也叫编译时多态。
2. 默认参数
- 在C++中函数的参数可以设置默认值,在函数调用时,如果调用者没有提供实参,则使用默认的形参。
- 如果只有一部分参数设置了默认形参,则设置的默认形参靠右排列。
- 如果函数的声明跟定义 分开实现,则只需要在函数的声明时设置默认形参。
- 默认形参会影响函数的重载,慎重给重载函数设置默认形参。
3. 内联函数
- 普通函数会在调用的位置生成调用(跳转)指令,当执行到调用代码时会跳转到函数所在的代码段位置执行。
- 内联函数就是把函数编译好的二进制指令拷贝到调用位置,也就没了跳转和返回的过程。
- 内联函数的优点与缺点:
- 与普通函数相比,内联函数的执行速度更快。
- 大量使用内联会使可执行文件变大,造成冗余(多份)。
- 显示内联 与隐式内联
- 显示内联 : 在函数的返回值类型前 加 inline(C语言也支持)
- 隐式内联 : 在结构,类,联合的成员函数会默认优化成内联函数。
- 是否内联 : 由编译器决定(部分编译器没有实现这个功能)。
- 宏函数在调用时,会把函数体直接 替换到调用位置 与内联函数一样,用空间换取时间。
- 宏函数与 内联函数的区别(优缺点)。
- 宏函数仅仅是代码替换不是真正的函数,也就不会由参数传递,入栈,出栈,返回值,而且所有的类型都可以使用,但不会进行类型检查,有安全隐患。
- 内联函数是真正的函数的调用,函数在调用时会进行参数的类型检查,传参,压栈,出栈,也可以有返回值,也可以重载,设置默认形参,但是这样就不能通用。
- 什么样的情况使用于内联函数
- 由于使用内联函数会使得可执行文件增大,增加内存开销,只有频繁调用且简单的短小的函数适合作为内联函数。
- 调用比较少且内容庞大复杂的函数,内联后并不能显著提升性能,不足以抵消牺牲空间换来的损失。所以不适合内联。
- 带有递归特性,和动态绑定的特性的函数,无法实施内联,因此编译器会自动忽略inline关键字。
六 堆内存管理
1. new/delete与malloc/free
-
new使用方法 :
- new 类型 例:int* p = new int;
- new会自动计算类型所需的字节数,然后从堆内存申请相对应的内存块,并放回内存块的首地址(有类型地址)
-
delete使用方法:
- delete 地址 例 : delete p;
- 释放内存中的内存块
注意: new/delete 与malloc/free 不能混用,因为 new/delete除了管理堆内存以外,还有其他的工作要做。
2. 数组的分配与释放
-
new 类型[n]; n表示数组的长度
-
delete[] 地址;
-
注意: 通过 new[]申请的内存,必须使用delete[] 释放,就是不能混用
3.重复释放
-
delete/delete[] 不能重复释放同一块内存
-
delete/delete[] 释放野指针的后果不确定,但释放空指针是安全的
4.内存申请失败
当内存申请过大,没有满足需要的整块内存时,会抛出异常。
"std::bad_alloc"
5. 区别
区别 | new/delete | malloc/free |
---|---|---|
身份 | 运算符 | 函数 |
参数 | 类型(自动计算字节数) | 字节数 |
返回值 | 带类型的地址 | 字节数 |
申请失败 | 抛出异常 | 返回NULL |
调用函数 | 构造/解析 | 无 |
6.相同点
- 都可以管理堆内存
- 不能重复释放
- 都可以释放空指针
七 强制类型转换
C++中有更安全的强制类型转换
1.静态类型转换(static_cast)
static_cast<目标类型>(源类型)
- 源类型和目标类型 只要有一个方向可以隐式转换,那么两个方向都可以做静态类型转换,如果不能则报错。
int num = 10;
static_cast<short>(num)
2.动态类型转换(dynamic_cast)
dynamic_cast<目标类型>(源类型)
- 源类型和目标类型必须同是 引用或指针,且目标类型的源类型之间存在继承关系,否则报错
3.去常态类型转换(const_cast)
const_cast<目标类型>(源类型)
- 源类型和目标类型必须同是 引用或指针,且目标类型的源类型只有常属性的区别,否则报错
- 区别在const有没有。
4.重解释类型转换(reinterpret_cast)
reinterpret_cast<目标类型>(源类型)
- 源类型和目标类型必须同是 指针+指针,或者 指针+整数,否则报错
5. 设计的初衷
本贾尼·斯特劳斯特卢普:如果让我重新设计,它们会更长。
原因: C++之父不希望程序员使用强制类型转换,如果程序中用到了强制类型转换则说明程序设计的不完善,程序员应该去优化程序的设计,而不应该强制类型转换。
八 引用
1. 什么是引用
引用就是区别名(外号),引用就是用一个新的标识符与另一个标识符的内存建立绑定关系。
#include <iostream>
using namespace std;
void swap(int& x,int& y)
{
int t=x;
x=y;
y=t;
}
int main(int argc,const char* argv[])
{
int x,y;
cin>>x>>y;
swap(x,y);
cout<<x<<y;
}
2. 引用的基本特性
- 引用必须初始化,也就是必须有引用目标。
- 不存在空的引用,但可能有悬空引用(变量已经销毁,但名还留着)
#include <iostream>
using namespace std;
int main(int argc,const char* argv[])
{
int* p = new int;
*p =1234;
int& x = *p;
// delete p; //销毁后不存在了,但名还在
cout<<p<<" "<<&x;
cout<<endl;
cout<<*p<<" "<<x;
}
- 可以引用无名对象或临时对象,但必须使用常引用。
#include <iostream>
using namespace std;
int main(int argc,const char* argv[])
{
const int& x =3+7 ;
cout<<x;
}
- 引用不能更换目标
- 引用一旦定义,初始化完成后就和普通变量一样了,他就代表了引用目标,一旦引用终身不能跟换引用目标(没有这种语法)
- 如果引用的目标具有const属性,那么引用也必须带有const。
#include <iostream>
using namespace std;
int main(int argc,const char* argv[])
{
const int x =7 ;
//int& t =x; //将类型为‘int&’的引用初始化为类型为‘const int’的表达式无效
const int& t =x;
cout<<t;
}
3 什么时候使用引用
- 参数型引用:
- 使用引用来做为函数的参数,能达到指针的效果(函数之间共享局部变量),但不具备指针的危险性。
- 函数的实现者有较大主动权,是否使用引用由函数的实现者(写函数的人)说了算。
- 引用当作函数的参数传参的效率更高,比指针还高,指针至少需要4字节的内存空间来存储变量的地址,而引用只是增加了一条标识符与内存的绑定记录。
- 返回值型引用
- 引用当作函数的返回值,传参的效率更高,但接收者也要使用引用来接,才能提高效率,否者还是内存拷贝。
- 但不要返回局部变量的引用,否则会造成悬空引用。
4. 引用与指针的区别(重要)
指针 | 引用 | |
---|---|---|
身份 | 数据类型 | 取名机制 |
与目标的关系 | 指向 | 绑定,映射 |
内存使用 | 4|8字节 | 无 |
危险系数 | 野指针,空指针,越界 | 悬空引用 |
- 引用的优缺点: 安全,效率高,但没有管理内存的能力
- 指针优缺点:无所不能,但是危险,需要额外的内存
指针 | 引用 |
---|---|
可以不初始化 | 必须初始化 |
可以有空指针 | 没有空引用 |
可以有二三级指针 | 没有多级引用 |
有指针数组 | 没有引用数组 |
有数组指针 | 有数组引用 |
有函数指针 | 有函数引用 |
九 面向对象编程
面向过程编程:关注于解决问题的方法,步骤
面向对象编程:关注于“谁(类)”能解决问题,解决问题需要的“数据(成员变量)”,以及解决问题需要的技能(成员函数)”。
-
抽象:想象出一个能解决问题的对象(观察,想象),分析出解决问题需要的属性(成员变量)和行为(成员函数)*。
-
封装:把抽象的结果封装成类(数据类型),并设置相应的访问权限。
- 使用封装的类实例化对象,调用对象的成员函数与成员变量相互作用,达到解决问题的目的。
-
**继承:**在解决问题之前,先寻找有没有能解决部分问题的类,如果有则把旧的类继承并再次扩展,缩短解决问题的时间,减低解决问题的难度。
-
**多态:**对象的多种形态,当对象发一条指令,对象会根据自身的情况做出独特响应。
十 类和对象
1.类
- 把抽象结果封装成一个数据类型(结构,类)
- 类就是一种数据类型
- 简单类型:只能表示一个变量,C/C++内建类型
- 数组类型:类型相同的多个变量
- 结构类型:不同类型的多个变量
- 类类型:不同类型的多个变量,函数
2.对象
- 类这种数据类型创建出实例,相当于结构变量。
十一 类的定义与实例化
1.类的一般形式
class 类名 :继承方式 父亲名;
{
访问限定符;
// 构造函数
//无参构造
类名(void) {}
//有参构造
类名(形参列表)
{
xxx = _xxx
xx = _xx
}
成员变量;
// 成员函数;
返回值 函数名(形参列表)
{
函数体;
return val;
}
}
2.访问限定符
- public:共有成员,可以在任何位置访问
- protracted:受保护成员,只能在本类中和子类中访问
- private:私有成员,只能在自己的成员函数访问
- 在class中成员默认是private,在struct中成员默认是public
- 一般把成员变量设置为private,把成员函数设置成public
3.对象的创建方法
- 在栈上创建 :
- 类名 对象;
- 类名 对象数组名[对象数量];
- 在堆上创建:
- 类名* 对象指针 = new 类名;
- 类名* 对象指针 = new 类名[对象数量];
十二 构造函数与初始化列表
1.什么是构造函数
- 对象创建时自动调用函数叫构造函数,负责完成一些准备工作。
2.无参构造
- 一个类中至少有一个构造函数,如果不显示实现,则编译器自动生成一个没有参数的构造函数也叫无参构造。
- 如果显示实现了构造函数,则不生成无参构造,为了避免出错,最好再显示实现一个什么都不做的无参构造。
3.有参构造
- 创建对象可以附带一些数据给构造用于初始化成员变量,这样就必须显示实现带参数的构造函数,构造函数的参数就是创建对象时传递的数据。
注意:
- 可以给有参构造设置默认参数,这样可以达到无参构造的效果。
4.单参构造
如果构造函数只有一个,如此参数的类型与类类型之间有类型提升的特性。
如果不想有类型提升的功能,可以在单参构造前加 explicit 关键,可以禁止单参构造的类型提升。
class Test
{
public:
Test(int n)
{
}
};
Test t = 10; 自动调用单参构造而不会出错。
5. 初始化列表
- 格式
构造函数(参数):成员1(参数1),成员2(参数2)//初始化列表
{
}
初始化列表就是一种给类的成员变量初始化的一种语法。
-
特点
-
可以区分同名的参数,和成员变量。
-
可以给类类型的成员变量的构造函数传递参数
-
可以给父类的构造函数传递参数
-
常属性的成员变量只能初始化列表中赋值
-
十三 对象的创建过程
-
分配类对象所需要的内存空间,无论栈或堆。
-
传递实参调用构造函数,完成如下任务:
- 执行初始化列表
- 根据继承表继承顺序调用父类的构造函数
- 根据成员变量的定义顺序调用成员变量的构造函数
- 执行构造构造函数中的代码
十四 类的声明与实现分开
1. 在头文件中声明类
class 类名:继承方式 父类
{
成员变量;
...
public:
类名(形参列表);
返回值 成员函数(形参列表);
...
};
2. 在源文件中实现类的成员函数
#include "声明类的头文件"
类名::类名(形参列表)
{
函数体;
}
返回值 类名::成员函数(形参列表)
{
函数体;
return val;
}
3. 注意:
-
使用类中只需要包含类的头文件,然后使用者与类的源文件目标文件一起合并即可。
-
如果类中的内容,可头文件全部实现。
类的成员变量初始化的一种语法。
-
特点
-
可以区分同名的参数,和成员变量。
-
可以给类类型的成员变量的构造函数传递参数
-
可以给父类的构造函数传递参数
-
常属性的成员变量只能初始化列表中赋值
-
十三 对象的创建过程
-
分配类对象所需要的内存空间,无论栈或堆。
-
传递实参调用构造函数,完成如下任务:
- 执行初始化列表
- 根据继承表继承顺序调用父类的构造函数
- 根据成员变量的定义顺序调用成员变量的构造函数
- 执行构造构造函数中的代码
十四 类的声明与实现分开
1. 在头文件中声明类
class 类名:继承方式 父类
{
成员变量;
...
public:
类名(形参列表);
返回值 成员函数(形参列表);
...
};
2. 在源文件中实现类的成员函数
#include "声明类的头文件"
类名::类名(形参列表)
{
函数体;
}
返回值 类名::成员函数(形参列表)
{
函数体;
return val;
}
3. 注意:
-
使用类中只需要包含类的头文件,然后使用者与类的源文件目标文件一起合并即可。
-
如果类中的内容,可头文件全部实现。