目录
1、C++简介
1)作者
1982 年,美国 AT&T 公司贝尔实验室的 Bjarne Stroustrup(本贾尼·斯特劳斯特卢普) 博士在 c 语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与 c 语言的渊源关系,它被命名为 C++。而 Bjarne Stroustrup博士被尊称 为 C++语言之父。
2)历史背景
C语言作为结构化和模块化的语言,在处理较小规模的程序时,比较得心应手。但是当问题比较复杂,程序的规模较大的时,需要高度的抽象和建模时,C语言显得力不从心。
为了解决软件危机,20 世纪80年代,计算机界提出OOP(object oriented programming)思想,这需要设计出支持面向对象的程序设计语言。Smalltalk 就是当时问世的一种面向对象的语言。而在实践中,人们发现C语言深入人心,使用非常广泛,以至于当时最好的方法,不是发明一种新的语言去取代它,而是在原有的基础上发展它。 在这种情况下 C++应运而生。最初这门语言并不叫 C++,而是C with class (带类的 c)。
自1982年被发明出之后,C++经过了多次完善,形成了现在的C++,当然C++还在扩展,每隔3年就会添加一些新的内容,目前面试/笔试提及最多的2011年发布的那一次标准。
3)应用领域
_1.系统层软件开发(操作系统,编译器等)
_2.服务器程序开发(网络上的服务器接收各种客户端请求)
_3.网络,游戏
_4.科学计算(使用计算机解决现代科学与数学中的数据计算)
如果你想一直在程序员这条路上走 =》 软件架构设计师
4)C++内容
C++语言的名字,如果看作C的基本语法,是由操作数C和运算符后++构成。C++ 是本身这门语言先是 C,是完全兼容 C.然后在此基础上++。这个++包含三大部分:
_1.C++对 C 的基础语法的扩展。
_2.面向对象(继承、封装、多态)。
_3.STL 以及 不定时发布的一些扩展内容。
我们C++的学习顺序也是按照这个内容来的,先了解C语言的扩展,再学习C++的核心,最后再学习C++的扩展.
在正式的内容之前,先装个c++的编译器:
平台不限制,你可以继续使用ubuntu,需要安装编译器 g++
sudo apt-get install g++ (如果在终端上敲命令 g++ -v 没反应,就敲下这个)
如果想直接在windows上用,可以装VS系列Visual Studio(2013/2015/2017/2019)
vs code 应该也是ok的,可以装个扩展包(平台不同,编译器的处理会有点不一样,所以有时某些代码的结果会不同)
交叉编译器 :arm-linux-g++ (能敲arm-linux-gcc 就能敲这个)
2.C++的第一个程序
C++代码文件的后缀:
源文件: .cpp .C (两种主流后缀)
头文件: .h .hpp 无后缀(主要体现标准库中的头文件,这个东西的产生主要和C产生区别)
程序示例代码:
#include<iostream> => 等效于stdio.h 但是这个是C++中输入输出的头文件
int main()
{
std::cout << "hello world" << std::endl;
return 0;
}:: 作用运算符,std是作用域的名字,cout 是作用域中的内容
{
int a;//a的作用域就是上下两个{}之内
{
int b;//b的作用域就是上下两个{}之内
}//c++允许作用域拥有名字 => 命名空间
}
cout 是标准输出流 => stdout
<< c语言表示左移,而在C++中允许我们重新定义运算符的使用规则
在cout << "hello world" 这段话中,则是表示把hello world 输入到标准输出流
endl => end line => c语言 \n "\n" == endl对于输出,你先记住用法:
cout << 输出值/常量/表达式 << 输出值/常量/表达式 << 输出值/常量/表达式 << 输出值/常量/表达式;
不同的输出值/常量/表达式以<<隔开代码的运行方式:
1.编译
g++ cpp文件列表 -o 可执行文件 (-E -S -c g++都是支持的)
编译流程: 预处理,编译,汇编,链接
2.运行代码
./可执行文件名 (后面也可以加 -I(头文件路径) -L(库文件路径) -l(库名))
5分钟,自己过一下上面的步骤
2_1 命名空间
c++允许作用域拥有名字 => 命名空间。
写法:
命名空间的定义格式:
namespace 标志符名字
{
表示命名空间的内容,这个内容几乎可以是任何内容,变量,函数,类型定义,甚至是子命名空间。
}
例子:
namespace test
{
int a = 10; //我建议是写声明,多文件使用会出问题。。。
int mysum(int a,int b)
{
std::cout << a+b << std::endl;
}
namespace child
{
int b = 12;
int test_func()
{
std::cout << "test" << std::endl;
}
}
}
用法:
3种:
直接通过 命名空间名::成员 去操作
test::a = 12; //直接修改命名空间中a的值
先添加作用域使用的声明语句,后续的使用不需要再加 命名空间名::
声明格式:
using 命名空间名::成员; 单个成员使用声明
using namespace 命名空间名; 该命名空间声明,之后成员的使用都不需要命名空间名:: 前缀
using test::a; => a = 12;
using namespace test; a = 12;
对于命名空间,我们用来避免命名冲突的,大型工程动不动就是上千个.c,函数名/变量名 光是命令就是头皮发麻,为了优化这种问题,我们常常把一个.c中的函数/类型定义/全局/静态变量等内容放到一个命名空间中,这样就能使得对象取名的的难度下降10倍以上。
示例代码:
test.cpp test.h输入操作:
#include<iostream>
using std::cin;
使用方式:
cin >> 变量 >> 变量 >> 变量;
示例代码见hello.cpp
//test.h文件
//pragma once =>C++头文件只需要这一句,就等效于下面3句话
#ifndef __TEST_H
#define __TEST_H
namespace test
{
int a;
int func();
int sum();
void prin();
}
#endif
//test.cpp文件
#include"test.h"
#include <iostream>
using std::cout;
using std::endl; //因为不知道std中到底有多少东西,一般只写要用的,免得重名操作
int test::a;
int test::func()
{
cout << "func" <<endl;
return 0;
}
int test::sum()
{
return 0;
}
void test::prin()
{
cout << "prin" <<endl;
return 0;
}
//hello.cpp文件
#include<iostream>//=> 等效于stdio.h 但是这个C++中输入输出的头文件
using namespace std;
//std是标准输入输出库对应的命名空间
/*
using std::cout;
using std::endl;//像这种用法,一般都是担心多个命令空间中有同名的
*/
namespace test
{
int a = 10;
int sum(int a,int b); //写个声明也是可以的
namespace child
{
int m = 100;
}
}
//如果函数的声明在一个命名空间中,那么定义时要说明命名空间的名字
int test::sum(int a,int b)
{
return a+b;
}
//如果只是定义命名空间,但是不声明 => 等同于没用
using test::child::m;
using test::sum;
int main()
{
cout << sum(m,12) << endl;
int value;
cin >> value;
cout << value << endl;
return 0;
}
3.C++在C语言上的内容加强
_1.变量检测加强
原先C中:
全局变量:
int a;// 当做是声明
int a = 100; //C语言没问题
C++这样写,会报错,因为int a,编译器将其看作是定义,如果你写了extern 那么任然是声明so,声明的时候最好+ extern。
_2.类型检测加强
函数参数与返回值问题:
对于C语言,函数可以不写返回值,那么会默认是int类型返回
函数可以空着参数,那么其实这个函数的实参列表可以填很多个
sum(int a,int b)
{
return a+b;
}//虽然编译器会警告,但是还是会给你附带上int返回值类型
void func()
{
}
func(1,2,3,4,5,6); //还是可以调用func函数而C++不允许一个函数没有返回值类型,且参数为空则默认是void
枚举检测:
enum xxx{M,N,Q,K};
枚举变量定义时,只能以内部成员名字初始化
_3.结构体加强
在原先的C结构体中,一般只能写变量/子结构体,而C++中,结构体中可以写函数,且这个函数是属于这个结构体的。
struct xxx => typedef struct xxx xxx;
{
int a;
int double_num(int a)
{
return a*2;
}
};
除了可以定义函数之外,结构体类型构建时,可以不写struct
struct xxx v;
xxx v;
_4.const加强
const 修饰词汇,被该词汇修饰的变量是一个只读的变量(只能放在=的右边),亦或者是叫一个常量。
C应用:
const int a = 10; //一个只读变量在定义时,必须赋予初始值
int b = 100;
int *const c = &b;// 这种情况,指针常量(指向的是一个常量),在这种情况下,你能进行的操作是*c
//但是你不能 c = &x; 不能重新赋值/指向其他的对象
const int *d = &b;//这种情况,常量指针,这种情况下,你不能进行的操作是*c = xxx;
//但是你能 c = &x;
如果const离变量近,表示变量的值不可改,指向的对象不可改!!
如果* 离变量近,表示指向对象的内容不可以改变!!!
在C语言中,一般来说,一个变量就算是常量,但是它依然会拥有一个地址,我们可以通过指针去修改这个只读变量的值(危险的,不要操作)
在C++中,常量是真的常量,一个变量在如果是一个只读变量,那么在编译期间,这个变量 =》 常量,这个变量的值将会保存在常量区,如果你试图通过指针修改这个常量的值,那么编译器会给出一个临时地址去给这个指针变量,以防止数据的修改。
const int a = 100;
int * b = (int *)&a;//指向常量,不可能的,给个替身给你
*b = 10;
cout << a << endl; 输出100
_5.bool类型
在C语言中,没有特定的类型表示逻辑真与假,只能通过类型的定义/宏去解决这个问题,C++则对此进行加强,衍生bool类型
bool类型是专门用于表示逻辑上的真与假的,所以在设定上只给与这个类型两个值以表示真假。
0 -> 假 false 1 -> 真 true 且两个英文单词就是这两个值对应的宏名
如果你试图赋予bool类型变量非0/1的值项,那么编译器会给你强转成0/1 => 实际上这个类型一般只用一个bit表示数据,但是这个类型一般占一个字节,如果你连续的定义了多个bool类型变量,那么可能有的编译器会把多个bool类型变量的数据保存在一个字节中。
定义格式:
bool 变量名=值;
示例代码:
bool a = 100;
bool b = 0;
4、C++在C语言上的扩展
_1.引用
在C语言中,往往存在着这种问题,如果我要一个局部变量的作用域(函数内变量)外访问它,那么都需要通过指针,使用指针必然涉及到*与&符号的频繁使用,对于指针理解不深刻的初学者可能就倒在了指针中,为了避免这种情况,C++提出了使用引用去替代一部分指针使用情况,从而使得变量的操作不再那么复杂(这主要体现在函数参数与返回值的使用上),当然如果你需要使用heap,那么指针是避免不了的
引用是什么?
变量的名字:变量的名字是变量所表示的那段存储空间的标志符,我们访问那段空间,一般都是通过变量名字(当然也可以通过指针),但是使用变量名更方便,而引用这个操作,是一个可以使一个变量拥有多个名字的一种操作。
引用的写法与用法:
定义格式:
数据类型 &变量名 = 已存在的变量名; //引用在定义时必须赋初值
以上面这种方式定义一个新变量之后,那么两个变量会指向同一个地址
例子:
int a = 100;
int &b = a;
int &c = b;
int &d = a;
a == b a与b表示同一个东西,操作a就是操作b
用途:
1.作函数参数,替代一级指针
2.作返回值,可作为左值使用
简单的看,就是给一个变量起别名:
_2.函数重载
重载 => 重复载入
函数重载 => 一个代码中,可以有多个函数名一样的函数
函数重载主要起到了,去除函数重名时编译器的报错问题,但是实际上是因为有人“帮忙解决了问题”。。。函数重载的写法:
C++允许函数名相同,但是"参数列表不同"的函数同时存在,返回值根本不需要考虑,参数列表不同 => 个数不同,参数类型不同(面试问点)
int sum(int a,int b)
{
return a+b;
}double sum(double a,double b)
{
return a+b;
}int sum(int a,int b,int c)
{
return a+b+c;
}函数的重载的实现方式:
C++编译器的函数换名机制 => 两种方式可验证
方式1: 看汇编代码
_Z3sumii:
_Z3sumdd:
_Z3sumiii:
方式2: 通过nm指令,看符号表。 nm xxx.o
0000000000000014 T _Z3sumdd
0000000000000000 T _Z3sumii
000000000000002e T _Z3sumiii作业:
仿函数重载方式,写两个函数
第一个函数 => 求整型一维数组的最小值
第二个函数 => 求字符一维数组的最小值
见homework.c
//homework.cpp文件
#include <iostream>
using namespace std;
/*
作业:
仿函数重载方式,写两个函数
第一个函数 => 求整型一维数组的最小值
第二个函数 => 求字符一维数组的最小值
*/
int intArrayMin(int *a, int n)
{
int min = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] < min)
{
min = a[i];
}
}
cout << min << endl;
return min;
}
unsigned char intArrayMin(unsigned char *a, int n)
{
unsigned char min = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] < min)
{
min = a[i];
}
}
cout << min << endl;
return min;
}
void func()
{
int n;
cout << "请输入数组长度" << endl;
cin >> n;
unsigned char*a=new unsigned char[n];//申请数组空间,int*p=new int[10];//10个元素空间
cout << "请输入数组元素" << endl;
for (int i = 0; i < n; i++)
{
cin >> a[i];
}
unsigned char min = intArrayMin(a, n);
cout << "最小值为:" << min << " ASCII值为:" << (int)min << endl;
}
int main()
{
func();
return 0;
}
_3.函数的默认参数
C++可以为函数的参数提供默认值,主要是为了解决一些函数的某个参数总是一个值的情况,但是偶尔会有所不同。
写法:
返回值类型 函数名(参数列表)
参数列表=> 类型1 名字1 = 默认值1, 类型1 名字2 = 默认值2
如果你不想给某个参数设置默认值,那么你应该把这个参数放在参数列表的前面,默认参数的原则是靠右原则:一旦当前参数设置了默认值,那么它右边的所有参数都需要设置默认值 。
为什么呢?,因为函数调用不存在 func(1, ,2) 这种情况
例子:
int sum(int a,int b,int c = 10,int d = 10)
{
return a+b+c+d;
}
int sum(int a,int b = 10,int c) //error
示例代码:
overload.c说明:函数调用时,没有对应的实参,则采用默认值,有实参就用实参值
ps:
int sum(int a,int b)
int sum(int a=10,int b = 10,int c = 10)
ambiguous => 模棱两可
默认值一般写声明中,不写函数定义中
//overload.cpp文件
#include<iostream>
using namespace std;
inline int sum(int a = 10, int b = 20);//用在声明中就行
int sum(int a, int b)
{
return a + b;
}
double sum(double a, double b)
{
return a + b;
}
int sum(int a, int b, int c)
{
return a + b + c;
}
int main()
{
cout << sum(1, 2, 3) << endl;
cout << sum(1) << endl;
cout << sum(1.2, 2.2) << endl;
}
_4.内联函数
int sum(int a,int b)
{
return a+b;
}int main()
{
int a,b;
for(int i = 0;i < 10;i++)
{
cin >> a >> b;
sum(a,b);
}//这是一段有病的代码,为什么呢,你为何要把一句话写成一个函数??
}//函数的调用 会有 栈创建、入栈、出栈、销毁栈等操作
//这4个操作花费的时间,肯定比a+b花的时间长,有必要考虑这么多吗?要的,这些小东西决定了你最后组装出的是拖拉机还是小奔(当然你放心,你现在去找工作不会问你这种东西,如果问了,他的意思是看你有没有代码优化的思想)所以在这种情况下,我们是可以把sum替换成a+b的,but有的时候,我不想这么干,有时写个函数能表达的意思肯定比一句话更完善,比如:
屏幕操作是不是有个画点函数:
draw_point(int x,int y,int color)
*(plcd + 800*y + x ) = color;
这种时候就会有一个矛盾的观念,我既想代码实际上是*(plcd + 800*y + x ) = color;,但是我也希望看着是draw_point(int x,int y,int color)解决这种问题,就需要使用到内联函数:
内联函数格式:
inline 返回值类型 函数名(参数列表){};inline这个关键字加在函数前面,这个函数就是一个内联函数
但是有个问题,我不能给你证明这个替换操作
这个替换操作是在编译时替换的 => 看汇编
虽然编译器知道你这个函数需要内联 => 但是它做不做要看编译器自己怎么处理 (至少简单的代码在g++上看不出结果)
并且需要注意的是,代码行数超过10行,就不需要内联了,没必要的这个东西看作是思想上的体现就行,因为实际操作要看编译器选择
5.C/C++混合编程
C++是从C语言上扩展出的,是向下兼容C语言的,所以在Cpp文件中,也可以完全写C形式代码,但是有时候,某些库函数接口是通过gcc编译器编译出来的,如果我们在cpp文件中使用这些函数,那么如果最终的代码使用g++编译 =》 出问题
test.c
int sum(int a,int b)
{
return a+b;
}test.h
int sum(int a,int b);gcc -share -fpic -o libtest.so test.c =>把这个c文件编译成了动态库
main.cpp
#include"test.h"
int main()
{
sum(1,2);
}china@ubuntu$ g++ main.cpp -ltest -L./
/tmp/cccchAaw.o:在函数‘main’中:
main.cpp:(.text+0xf):对‘sum(int, int)’未定义的引用
collect2: error: ld returned 1 exit status为什么会找不到呢? 重载 => 函数换名
g++ 编译器会给函数换名,但是gcc编译器不会,所有两个编译器编出来的函数格式不对,就找不到。
解决这种问题的方式 => 修改头文件中的声明
当你使用的函数是c编译器编出来的,那么你需要使用特殊的声明语句包含内容
extern "C" //这个声明语句表示,接下来{}中的内容将会使用C的方式去找
{}
#ifdef __cplusplus //使用C++编译,该宏定义,所以extern "C"有效,但是C编译器,不带这个宏,extern "C"无效
extern "C" //这个声明格式是C++专属的,C语言编译器不认识
{
#endif
int sum(int a,int b);
#ifdef __cplusplus
}
#endif
=》详细情况 =》test2.h自己敲一下,虽然这个东西目前用的少,还是留点印象。
//test2.h文件
#ifndef __TEST_2
#define __TEST_2
//在C++编译器中有一个预定义的宏 __cplusplus ,当你使用了c++编译器时,肯定会有这个宏的定义
#ifdef __cplusplus //使用C++编译,该宏定义,所以extern "C"有效,但是C编译器,不带这个宏,extern "C"无效
extern "C" //这个声明格式是C++专属的,C语言编译器不认识
{
#endif
int sum(int a,int b);
#ifdef __cplusplus
}
#endif
#endif
//main.cpp文件
#include"test2.h"
#include<iostream>
using namespace std;
int main()
{
cout << sum(1,2) << endl;
}
Thank you, Mr. T.Z. for teaching me this lesson, on September 7, 2021, at the beginning of my senior year.
End2022,3,9