1.C++的第一个程序
之前写的C语言文件都是后缀为.c的文件,进入C++后就要把后缀改为.c++了,vs编译器看到是.cpp就会调⽤C++编译器编译。C++兼容C语言的绝大多数语法,所以C语言的 hallo word 依旧可以在C++下使用。
//test.cpp
//c语言的hallo world
#include<stdio.h>
int main() {
printf("%s", "hallo world\n");
return 0;
}
当然C++也有自己的输入输出,C++的hallo word长这样:
//test.cpp
//c++的hallo world
#include<iostream>//相当于c语言的.h文件
using namespace std;
int main() {
cout << "hello world\n";
return 0;
}
2.命名空间
2.1命名空间的价值
在C/C++ 中,标准库中的函数命名与程序员写的函数命名、程序员和程序员之间的(函数,变量,宏等等)这些可能会存在命名冲突,使用命名空间(namespace)可以有效避免命名冲突,使得代码更加模块化和易于管理。
c语⾔项⽬类似下⾯程序这样的命名冲突是普遍存在的问题,C++引⼊namespace就是为了更好的解决这样的问题
//c语言的命名冲突
#include<stdio.h>
#include<stdlib.h> //rand()函数在这个库中,编译链接后展开
int rand = 10;
int main() {
//C2365 "rand":重定义,以前的定义是"函数"
printf("%d", rand);
return 0;
}
2.2命名空间的定义
- 命名空间的定义需要用到namespac关键字,后面接命名空间的名字,然后在加一对{}即可。{}中即为命名空间的成员。命名空间可以定义变量/函数/类型等。
- 命名空间的本质是定义了一个新的域,这个域跟全局域互相独立,不同的域可以定义同名变量。这样在命名空间中定义一个rand变量就不会和全局域中的rand函数冲突了。
- C++中有函数局部域,全局域,命名空间域类域,类域。域影响的是编译器在编译时查找一个变量/函数/类型出处(定义或声明)的逻辑,所以有了域的隔离,名字冲突问题自然就解决了。局部域和全局域除了会影响查找逻辑,还会直接影变量的声明周期,而命名空间域和类域不会直接影响变量的生命周期。
- 命名空间只能定义在全局,它也可以嵌套定义。
- 项目工程中的多个文件,会将同名的命名空间当成一个,不会有冲突
- C++的标准库都放在一个std(standard)的命名空间域中
普通的命名空间域定义
#include<iostream>
#include<stdlib.h>
//普通的命名空间定义 jiuwu是这个命名空间的名字
namespace jiuwu
{
int a = 10;
int ADD(int a, int b)
{
return a + b;
}
struct Node
{
struct Node* next;
int val;
};
}
int main() {
//这的rand是全局域中的rand函数
printf("%p\n", rand);
//这是jiuwu命名空间域中的rand变量
printf("%d\n",jiuwu::a);
return 0;
}
嵌套定义命名空间域
#include<iostream>
#include<stdlib.h>
//嵌套命名空间定义
namespace XiangMu_1
{
namespace jiuwu
{
int a = 10;
int ADD(int a, int b)
{
return a + b;
}
struct Node
{
struct Node* next;
int val;
};
}
namespace sidangkang
{
int a = 20;
int ADD(float a, float b)
{
return a + b;
}
struct Node
{
struct Node* next;
int val;
};
}
}
int main() {
//这的a是命名空间XiangMu_1中的嵌套jiuwu中的a
printf("%d\n", XiangMu_1::jiuwu::a);
//这的a是命名空间XiangMu_1中的嵌套sidangkang中的a
printf("%d\n", XiangMu_1::sidangkang::a);
return 0;
}
2.3命名空间的使用
编译器在查找一个变量的声明/定义时,默认只会在局部域和全局域中查找,不会到命名空间域中查找,所以以下程序会报错
include <stdio.h>
namespace jiuwu
{
int a = 0 ;
int b = 1 ;
}
int main ()
{
// 编译报错: error C2065: "a": 未声明的标识符
printf ( "%d\n" , a);
return 0
}
我们要使用命名空间中定义的变量/函数,有三种方式:
-
指定命名空间访问,项目中推荐这种方式。
-
using将命名空间中某个成员展开,项目中经常访问的不存在冲突的成员推荐这种方式。
-
展开命名空间中全部成员,项目不推荐,冲突风险很大,日常小练习程序为了方便推荐使用。
namespace jiuwu
{
int a = 0;
int b = 1;
}// 指定命名空间访问
int main()
{
printf("%d\n", jiuwu::a);
printf("%d\n", jiuwu::b);
return 0;
}// using将命名空间中某个成员展开
using jiuwu::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}//将整个命名空间域展开
using namespce jiuwu;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
3.C++的输入&输出
-
<iostream> 是C++的标准输入/输出库,定于了标准输入输出对象
-
std::cin 是istream的对象,它主要面向窄字符的标准输入流
-
std::cout是ostream的的对象,它主要面向窄字符的标准输出流
-
std::endl是一个函数,流插入输出时,相当于一个换行字符加刷新缓冲区
-
<<是流插入运算符,>>是流提取运算符 (c语言还使用它们做为移位运算符)
-
C++的输入输出非常方便,它不用向printf函数那样指定类型,它可以自己识别类型
-
cout/cin/endl都属于C++的标准库,C++的标准库都放在一个叫std(standard)的命名空间中,所以要同命名空间的使用方法去使用它们
-
<iostream>间接包含了<stdio.h>,使用可以直接使用printf和scanf,vs系列的编译器是这样的,其他编译器可能会报错
#include<iostream>
int main() {
int a;
float b;
char c;//cin = scanf //>>从标准输入流中提取数据 std::cin >> a >> b >> c; //可自动识别数据类型 //cout = printf //<<将数据输入到标准输出流中 std::cout << a << " "<< b <<" " << c <<std::endl; return 0;
}
4.缺省参数
- 缺省参数就是在定义或声明函数时为函数的形参指定一个默认值。在调用参数时,如果没有对应的实参那么就会使用设定好的缺省值,否则使用实参。缺省参数右分为全缺省参数与半缺省参数。
- 全缺省参数就是将函数所有的新参都设计为缺省参数,半缺省参数就是将函数部分参数设置为缺省参数。C++规定半缺省参数只能从右往左连续缺省。
- 带缺省参数的函数的调用,C++规定只能从左往右连续给实参,不能跳跃给实参。
- 函数声明定义分离时,规定缺省参数必须在声明中给。
cpp
#include<iostream>
using namespace std;
//全缺省
void func(int a = 10, int b = 10, int c = 10)
{
cout << a << endl << b << endl << c;
}
//半缺省
void func1(int a, int b = 10,int c = 10)
{
cout << a << endl << b << endl << c;
}
//错误半缺省
void func2(int a = 10, int b, int c)//应从右往左连续设置缺省值
{
cout << a << endl << b << endl << c;
}
int main() {
func();//在没有实参时使用设计好的缺省值
func(1);//传参时,使用指定实参
func1();//不能再没有设置缺省参数的地方,选着不传实参
func1(1);//这里至少传一个实参
func1(1, 2);
return 0;
}
5.函数重载
C++中规定可以在同一作用域中出现同名函数,但是要求它们的形参不同
cpp
//函数重载
// 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(int left, int right)" << endl;
return double + double;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数顺序不同
void f(char b,int a)
{
cout << "f(char b,int a)" << endl;
}
void f(int a,char b)
{
cout << "f(int a,char b)" << endl;
}
//注意!不能仅通过返回值,与返回类型进行函数重载
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(int left, int right)//返回类型不能作为重载的条件
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
6.引用
6.1什么是引用
引用不是新定义一个变量,而是给现有的变量取一个别名,这个别名并不会新开辟一块内存空间,而是会和被取别名的变量一起使用一块空间。
类型 & 引用别名 = 引用对象
#include<iostream>
using namespace std;
int main() {
int a = 10;
//给a这个变量取别名
int& b = a;
int& c = a;
//也可以给别名取别名,但终究还是同一个变量】
int& d = b;
//这里我们可以发现它们的内容和地址都是完全一样的
cout << a << " " << &a << endl;
cout << b << " " << &b << endl;
cout << c << " " << &c << endl;
cout << d << " " << &d << endl;
return 0;
}
6.2引用的特性
1.引用在定义时必须初始化
2.同一个变量可以有多个引用
3.引用一旦被初始化,就不能再改变其指向的对象
4.由于:引用一旦被初始化,就不能再改变其指向的对象,所以在使用const修饰引用时只能修饰其"内容"
#include<iostream>
using namespace std;
int main() {
//引用在定义时必须初始化,否则报错
//int& a;
//一个变量可有多个引用
int b = 10;
int& c = b;
int& d = b;
//这⾥不是让f引⽤b,因为C++引⽤不能改变指向,这里是给f赋值即 f = 10;e = 10
int e = 20;
int& f = e;
cout << f<<endl;
f = b;
cout << f<<endl;
//const修饰引用
const int& ra = a;//正确
int& const ra = a;//错误 原因:引用本质上是一个已经绑定了某个对象的别名,其"指向"在初始化后是不可变的。因此,为引用添加 const 修饰其"指向"是没有意义的,也是不被编译器允许的。
return 0;
}
6.3const引用
当引用一个const对象,必须使用const引用 。const引用 也可以引用普通对象,因为对象的访问权限可以在引用时缩小,但是不能在引用时放大。
当我们引用编译器创建的临时对象时,也需要用const引用。因为临时对象体通常是只读的,对它进行引用而不使用const引用会造成权限的放大
临时对象的产生
1.表达式的结果:
某个表达式的结果会被编译器会创建一个临时对象来保存这个结果
//10 + 20 的结果会被存储到临时对象中,然后在赋值给a int a = 10 + 20;
2.类型转换:
当执行显式的类型转换或隐式的类型转换,且转换结果需要被存储时,也可能产生临时对象
//10.2类型转换的结果会被存储在临时对象中,然后在被赋值被a int b = 10.2;
3.常量引用的绑定:
// 编译器会创建一个值为10的临时int对象 const int& a = 10;
临时对象的生命周期:临时对象的生命周期是自动管理的,并且通常与引用它的作用域相同。一旦引用超出了它的作用域,临时对象就会被销毁。
6.4.指针和引用的区别
- 在语法层面,引用的创建不需要开空间属于引用对象的别名,而指针的创建需要空间
- 引用在定义时必须初始化,而指针不用(指针不初始化容易造成野指针问题)
- 引用在指向一个对象后不能在更改,而指针可以随时更换指向对象
- 引用可直接访问指向对象,而指针需要解引用
- sizeof中的含义不同,引用结果为引用对象类型大小,但指针永远是固定的字节(在32位系统上,指针的大小通常是4字节,在64位系统上,指针的大小通常是8字节)
7.内联函数
内联函数只需要在函数前面加上inline关键字,编译器会在有内联函数的地方选择性的展开,这样就不用新开栈帧了,对效率提升有所帮助
内联函数的展开对于编译器来说只是一个建议,对于程序员不合理的展开,编译器会选择视而不见
内联函数的声明和定义不建议分离在不同的源文件中,这样会导致链接错误。因为inline展开就没有地址,编译器就找不到定义的内联函数
在vs编译器debug状态进行反汇编码的观察,我们发现内联函数并未展开,这是为了方便程序员调试,debug状态下展开内联函数需要设置这两个地方
#include<iostream>
using namespace std;
// 正确的宏实现
#define ADD(a, b) ((a) + (b))
// 为什么不能加分号?
// 为什么要加外⾯的括号?
// 为什么要加⾥⾯的括号?
int main() {
cout << ADD(2, 3) << endl;//如果加了分号就会出现语法错误
cout << ADD(2, 3) * 4 << endl;//如果不加外面括号->(a)+ (b) * 4,b会先与4乘法运算,跟我们实际需求不符
int a = 1, b = 2;
cout << ADD(a | b, b & a) << endl;//如果不加里面的括号->(a | b + b &a)b 会与 b先进行加法运算
return 0;
}
8.nullptr
在C++和C中,NULL实际上是一个宏,C++中NULL被替换成 0,C中NULL被替换成(void*)0。但是无论这两种定义,在C++中使用它们的时候都会出一些问题,例如:
void func(int); void func(int*);
假如你想调用 void func(int*);你可能会这样做:
func(NULL);
但是由于NULL被替换成了字面量0,所以你实际上调用的是int版本的func函数
你还可能会这样做:
funk((void*)NULL);
这样写在C++中也是错误的,因为在C++中void*指针不能够隐形的转化为其他类型的指针,在C语言中这样写是可以的,因为C语言中的void*指针可以给给任意类型的指针
所以在C++11中引入了nullptr关键字。nullptr实际上是一种特殊的字面量,它能够隐形的转化成任意类型的指针,而不能过转化成整数类型
#include<iostream>
using namespace std;
void func(int a)
{
cout << "func(int a)" << endl;
}
void func(int* a)
{
cout << "func(int* a)" << endl;
}
int main() {
func(NULL);
//func((void*)NULL);
func(nullptr);
return 0;
}