C++基础(1)
目录
1. C++发展历史
2. C++第一个程序
3. 命名空间
3.1 namespace的价值
3.2 命名空间的定义
3.3 命名空间的使用
4. C++输入和输出
5. 缺省参数
6. 函数重载
6.1 实现函数重载的条件
6.2 函数重载的应用
1. C++发展历史
C++的起源可以追溯到1979年,当时BjarneStroustrup(本贾尼·斯特劳斯特卢普,这个翻译的名字不同的地方可能有差异)在贝尔实验室从事计算机科学和软件工程的研究工作。面对项目中复杂的软件开发任务,特别是模拟和操作系统的开发工作,他感受到了现有语言(如C语言)在表达能力、可维护性和可扩展性方面的不足。
1983年,BjarneStroustrup在C语言的基础上添加了面向对象编程的特性,设计出了C++语言的雏形, 此时的C++已经有了类、封装、继承等核心概念,为后来的面向对象编程奠定了基础。这一年该语言被正式命名为C++。
随着C++的应用日益广泛,对其进行标准化的需求也越来越迫切。1990年,C++的标准化工作正式启动,由美国国家标准协会(ANSI)和国际标准化组织(ISO)共同负责。
C++的标准化过程是一个充满挑战和争议的过程。在长达八年的时间里,来自不同国家和背景的开发者们就C++的语法、语义、库和运行时环境等方面进行了深入的讨论和协商。最终,在1998年,C++的第一个国际标准ISO/IEC 14882:1998正式发布,标志着C++的正式成熟和广泛应用。
2. C++第一个程序
C++兼容C语言绝大多数的语法,所以C语言实现的helloworld依旧可以运行,C++中需要把文件后缀改为.cpp,vs编译器看到是.cpp就会调用C++编译器编译,linux下要用g++编译,不再是gcc。
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
当然C++有一套自己的输入输出,严格说C++版本的helloworld应该是这样写的:
#include<iostream>
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
3. 命名空间
3.1 namespace的价值
在C/C++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称将都存在于全局作同域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
如下面C语言程序存在的命名冲突问题:
#include<stdio.h>
#include<stdlib.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
运行结果:
运行时编译报错,编译时头文件<stdlib.h>中有一个rand()函数,是用来产生随机数的一个函数,与我们定义的变量rand名字一样,就产生重定义,为了避免这种情况发生,namespace关键字闪亮登场。
3.2 命名空间的定义
- 定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
- namespace本质是定义出一个域,这个域跟全局域各自独立,不同的域可以定义同名变量,所以下面的rand变量就不存在冲突了。
- C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找一个变量/函数/ 类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。
- namespace只能定义在全局,当然还可以嵌套定义。
- 项目工程中多文件中定义的同名namespace会认为是一个namespace,不会冲突。
- C++标准库都放在一个叫std(standard)的命名空间中。
#include<stdio.h>
#include<stdlib.h>
namespace dog //dog是命名空间的名字,⼀般开发中是⽤项⽬名字作为命名空间名字。
{
int rand = 10;
}
int m = 5;
int main()
{
printf("%p\n", rand); //stdlib.h头文件rand变量是一个指针,我们用%p打印
printf("%d\n", dog::rand); //::域作用限定符 指定命名空间中的rand,打印10
int m = 55;
printf("%d\n", m); //局部变量优先,打印55
printf("%d\n", ::m);//访问全局变量m,打印5
return 0;
}
运行结果:
//命名空间中可以定义变量/函数/类型
#include<stdio.h>
#include<stdlib.h>
namespace dog
{
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
printf("%d\n", dog::Add(1, 2)); //3
struct dog::Node p1;//定义结构体变量
return 0;
}
//命名空间可以嵌套定义,解决一个命名空间存在同名变量问题
#include<stdio.h>
#include<stdlib.h>
namespace dog
{
namespace cat
{
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
}
namespace tiger
{
int rand = 20;
int Add(int left, int right)
{
return (left + right) * 10;
}
}
}
int main()
{
printf("%d\n", dog::cat::rand); //10
printf("%d\n", dog::tiger::rand);//20
return 0;
}
3.3 命名空间的使用
编译查找一个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间里面去查找。所以下面程序会编译报错:
#include<stdio.h>
namespace dog
{
int a = 0;
}
int main()
{
printf("%d\n", a);
return 0;
}
运行结果:
所以我们要使用命名空间中定义的变量/函数,有三种方式:
- 指定命名空间访问,项目中推荐这种方式。
- using将命名空间中某个成员展开,项目中经常访问的不存在冲突的成员推荐这种方式。
- 展开命名空间中全部成员,项目不推荐,冲突风险很大,日常为了方便练习程序推荐使用。
#include<stdio.h>
namespace dog
{
int a = 0;
int b = 1;
}
int main()
{
printf("%d\n", dog::a); //指定命名空间访问
return 0;
}
#include<stdio.h>
namespace dog
{
int a = 0;
int b = 1;
}
using dog::a; //using将命名空间中某个成员展开
int main()
{
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", dog::b);
return 0;
}
#include<stdio.h>
namespace dog
{
int a = 0;
int b = 1;
}
using namespace dog; //展开命名空间中的全部成员
int main()
{
printf("%d\n", a);
return 0;
}
4. C++输入和输出
- <iostream> 是Input Output Stream的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。
- std::cin 是istream类的对象,它主要面向窄字符(narrow characters (of type char))的标准输入流。
- std::cout 是ostream类的对象,它主要面向窄字符的标准输出流。
- std::endl 是一个函数,流插入输出时,相当于插一个换行字符加刷新缓冲区。
- << 是流插入运算符,>> 是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)
- 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是通过函数重载实现的),其实最重要的是 C++的流能更好的支持自定义类型对象的输入输出。
- cout /cin /endl 等都属于C++标准库,C++标准库都放在一个叫std(standard)的命名空间中,所以要通过命名空间的使用方式去用他们。
- 一般日常练习中我们可以展开命名空间中全部成员 (using namespace std),实际项目开发中不建议。
- 在vs中没有包含<stdio.h>,也可以使用printf 和scanf,在包含<iostream>间接包含了,vs系列编译器是这样的,其他编译器可能会报错。
#include<iostream>
using namespace std;
int main()
{
int a = 0;
double b = 3.1;
char c = 'y';
cout << a << " " << b << " " << c << endl;
std::cout << a << " " << b << " " << c << endl;
cout << endl;
//可以自动识别变量的类型
cin >> a >> b >> c;
cout << a << " " << b << " " << c << endl;
return 0;
}
运行结果:
//C++兼容C语言,如果想控制精度打印的话可以直接使用printf
#include<iostream>
using namespace std;
int main()
{
double b = 22345.666666;
printf("%.2lf\n", b);
cout << b << endl;//cout 输出默认精度为 6 位有效数
return 0;
}
运行结果:
C++ IO的性能
C++为了兼容C语言,会做出一些妥协优化。C语言的缓冲区只有遇到刷新标志时才会进行刷新,而如果printf缓冲区还没有刷新,我们使用
cout
会出现什么情况?会先把printf
缓冲区刷新出来,再打印cout
输出的内容,所以cout
之前会先对缓冲区进行检查!所以C++风格IO需要和C风格IO进行缓冲区同步!
对于有大量IO的场景,C++的IO效率会比C风格IO慢,可以进行下面优化:
#include<iostream>
using namespace std;
int main()
{
// 在IO需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码,可以提⾼C++ IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
5. 缺省参数
- 缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参,缺省参数分为全缺省和半缺省参数。(有些地方把缺省参数也叫默认参数)
- 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左 依次连续缺省,不能间隔跳跃给缺省值。
- 带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。
- 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
- 缺省值必须是常量,C语言不支持缺省参数(编译器不支持)。
#include <iostream>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); //没有传参时,使⽤参数的默认值
Func(10); //传参时,使⽤指定的实参
return 0;
}
运行结果:
第一个形参没有给出缺省参数值,至少传入一个实参。
第一、二个形参无缺省参数值,至少传入两个实参。
三个形参均无缺省参数值,需传入三个实参 。
#include <iostream>
using namespace std;
//全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
//半缺省
void Func2(int a, int b = 10, int c = 20)//从右往左 依次连续缺省,不能间隔跳跃
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{ //从左到右依次给实参
Func1();
Func1(1);
Func1(1, 2);
Func1(1, 2, 3);
Func2(100);
Func2(100, 200);
Func2(100, 200, 300);
return 0;
}
运行结果:
6. 函数重载
C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了多态行为,使用更灵活。C语言是不支持同一作用域中出现同名函数的。
6.1 实现函数重载的条件
- 参数个数不同
- 参数类型不同
- 参数顺序不同
#include<iostream>
using namespace std;
// 1、参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
int main()
{
Add(1, 2);
Add(1.1, 2.2);
f();
f(5);
f(8, 'm');
f('n', 4);
return 0;
}
运行结果:
#include<iostream>
using namespace std;
//下面两个函数构成重载
//但是调用fg()时会产生歧义,编译器不知道调用谁
void fg()
{
cout << "fg()" << endl;
}
void fg(int a=10)
{
cout << "fg(int a)" << endl;
}
int main()
{
fg();
return 0;
}
运行结果:
这里无参调用时存在歧义,没办法区分,所以在书写函数时不要一个无参,另一个全缺省。
#include<iostream>
using namespace std;
//返回值不同不能作为重载条件
void function()
{}
int function()
{
return 0;
}
int main()
{
function();
return 0;
}
运行结果:
返回值不同不能作为重载条件,因为调用时也无法区分。
如果这样调用函数:int x = function (); 通过接受返回值则可以判断出function 是第二个函数,如果我们不接收函数的返回值,在这种情况下,编译器和程序员都不知道哪个function 函数被调用。所以只能靠参数而不能靠返回值类型的不同来区分重载函数。
6.2 函数重载的应用
对于栈这种数据结构,我们定义一个栈的结构体和对它初始化的函数。
//Stack.h
#include<iostream>
#include<assert.h>
using namespace std;
typedef int SLDataType;
typedef struct Stack
{
SLDataType* a;//指向动态开辟的数组
int top;//确定栈顶位置
int capacity;//栈的容量
}ST;
//初始化
void STInit(ST* pst, int n = 4); //缺省参数不能声明和定义同时给
//Stack.cpp
#include"Stack.h"
void STInit(ST* pst, int n) //函数声明和定义
{
assert(pst);
pst->a = (SLDataType*)malloc(sizeof(SLDataType) * n);
pst->top = 0;
pst->capacity = n;
}
//Test.cpp
int main()
{
//确定知道要插⼊1000个数据,初始化时一把开好,避免扩容,效率得到提升
ST s1;
STInit(&s1, 1000);
return 0;
}
如果我们要开确定容量的空间,直接手动传入确定容量,可以避免多次扩容和空间浪费,如果不知道确定要开多少空间,此时可以不传入实参,默认使用给的缺省参数值,如果空间不够就继续扩容。