C++入门
- 1. 域
- 2. 命名空间
- 2.1命名空间的定义
- 2.2 命名空间的使用
- 3. C++输入和输出
- 4. 缺省参数
- 5. 函数重载
- 6. 引用
- 7. auto
- 8. 范围for
- 9.nullptr空指针
- 10.内联函数
1. 域
- 域就是作用域,同一个域不可以用同名的变量,不同域可以用同名变量,遵循局部优先。域包括局部域、全局域、类域、命名空间域。其中我们学过局部域和全局域,类域和命名空间域后面会讲到。
- 域的访问
int a = 0;
int main()
{
int a = 1;
printf("%d", a);//a的作用域是局部域
//如何打印全局域的a?
//需要用到域作用限定符::
printf("%d", ::a);
return 0;
}
2. 命名空间
在C语言中,我们定义的变量、函数的名字在全局域可能与库中函数发生冲突,或者跟其他文件项目的变量名发生冲突。而C++的命名空间就可以解决这个问题。命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染。
2.1命名空间的定义
namespace 命名空间的名字
{
命名空间的成员
}
- 命名空间的成员可以是变量、函数、自定义类型。
//例子
namespace nanmu
{
int a;
void Swap(int*p1,int*p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
struct student
{
char name[20];
int age;
}
}
- 命名空间可以嵌套定义
namespace N1
{
int a;
int b;
namespace N2
{
int a;
int b;
int Add(int x,int y)
{
return x+y;
}
}
}
-
同一个工程允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间。例如一个.h文件和一个.c文件都定义了一个命名空间N1,最终都会合并成一个。
注意
名称相同的命名空间合并后有两个同名的变量/函数/自定义类型,就会报错(同一个作用域不可以用同名的函数)。怎么解决?再定义一个命名空间嵌套。 -
命名空间域是命名空间成员的作用域。
2.2 命名空间的使用
我们要如何使用命名空间的成员?以下是三种命名空间的使用方式:
- 使用using namespace 命名空间的名称(全部展开)
using namespace std;
//std是C++标准库的命名空间名,它包括C++库和STL(以后会学到)
int main()
{
printf("hello,c++");
}
直接展开命名空间后,如果我们定义和库重名,就会发生冲突。所以就有第二和第三种方式。
- 使用using将命名空间的某个成员引入(展开某个)
using N1 a;
int main()
{
printf("%d",b);
}
- 使用命名空间+作用域限定符+成员访问
int main()
{
printf("%d",N::a);
}
结论:直接展开有风险,我们定义如果跟库重名,就会报错。建议项目里面不要展开,日常练习可以直接展开。
3. C++输入和输出
using namespace std;
#include<iostream>
int main()
{
int a = 0;
cin >> a;//从键盘输入a
cout << a << endl;//打印a到屏幕
return 0;
}
cin是标准输入对象(键盘),cout是标准输出对象(控制台),endl是C++符号,表示换行输出,<<是流插入符号,>>是流提取符号。< iostream >是cin、cout、endl的头文件。
- 跟C语言的输入输出对比(scanf,printf),C++的输入输出更方便,不用手动控制格式,C++会自动识别变量类型。
int main()
{
char a;
int b;
double c;
cin >> a >> b >> c;
cout << "char:" << a << endl << "int:" << b << endl << "double:" << c << endl;
return 0;
}
- scanf要快于C++的输入(cin)。原因是C++兼容C,要在缓冲区兼顾C和C++的输入。
4. 缺省参数
如果我们在函数传参时,忘记传实参或者少传参了怎么办?C语言会报错,而C++提出了缺省参数。
- 概念
缺省参数(又叫默认参数)是在函数声明和定义时为函数参数指定一个缺省值(默认值)。 - 作用
在函数传参时,如果没有指定实参就使用该参数的缺省值。如果给定实参就不使用
int Add(int x = 1, int y = 1)
{
return x + y;
}
int main()
{
int ret = Add();
cout << ret << endl;
return 0;
}
结果
- 分类
//全缺省参数(所有参数都给缺省值)
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
//半缺省参数(缺省部分参数)(至少需要传一个参数)
void Func2(int x , int y = 2)
{
cout << "x = " << x << endl;
cout << "y = " << y << endl;
}
int main()
{
Func1();
Func2(2);
return 0;
}
结果
- 注意
(1)我们给参数缺省值的时候,必须从右往左给,因为我们传参时是从左往右传。
(2)缺省参数不能在函数声明和定义中同时出现。如果我们声明和定义的函数参数的缺省值不一样,编译器就无法确定哪个才是真正的缺省值。
(3)那么声明和定义都给相同的缺省值?还是只给一个缺省值就够了?答案是只给一个就够了。那么是声明给缺省值,还是定义给缺省值?答案是声明给缺省值。定义给缺省值就会报错,在编译阶段,文件会包含头文件,此时头文件中的函数声明未给缺省参数,就会报错,所以是声明给。
(4)缺省值必须给常量或者全局变量
- 应用
struct Stack
{
int* a;
int top;
int cap;
};
void StackInit(struct Stack* p, int capa = 10)
{
……
}
int main()
{
struct Stack s1;
StackInit(&s1,100);//如果我们已知道要建立一个大小为100的栈,这时就直接将参数传过去
struct Stack s2;
StackInit(&s2);//如果不知道要创建大小为多少的栈,这时可以不传参,用缺省值
return 0;
}
5. 函数重载
- 概念
C++允许同一作用域出现同名函数,这些函数的参数个数、参数类型、参数顺序不同,构成功能类型的重载函数。(对函数的返回值没有要求,可相同可不同)
//函数重载
int Add(int x, int y)
{
return x + y;
}
double Add(double x, double y)
{
return x + y;
}
int main()
{
int ret1 = Add(1, 2);
double ret2 = Add(1.5, 2.5);
cout << "ret1 = " << ret1 << endl;
cout << "ret2 = " << ret2 << endl;
return 0;
}
结果
- 分类
//1.参数类型不同
int Add(int x, int y)
{
return x + y;
}
double Add(double x, double y)
{
return x + y;
}
//2.参数个数不同
void Func1(int a)
{
cout << "Func1(int a)" << endl;
}
void Func1(int a, int b)
{
cout << "Func1(int a,int b)" << endl;
}
//3.参数顺序不同
void Func2(int a, double b)
{
cout << "Func2(int a,double b)" << endl;
}
void Func2(double a, int b)
{
cout << "Func2(double a ,int b)" << endl;
}
- 注意
(1)调用歧义
vodi f()
{
cout<<"void f()"<<endl;
}
void f(int a = 0)
{
cout<<"void f(int a = 0)"<<endl;
}
int main()
{
f();
}
首先,f()和f(int a =0)是重载函数。但是,你能明白f()调用哪个函数吗?它既可以解释为不传参的函数,此时它调用的是有缺省值的函数;也可以理解为无参数的函数,此时它调用的是无参数的函数。两个都可以调用就存在歧义,编译器就会报错。
(2)函数名问题
void f(int a,int b)
{
……
}
void f(int b,int a)
{
……
}
这是否构成函数重载?不构成,这并不是函数参数的顺序不同,它跟形参的名字不同无关。
- 为什么C++支持函数重载?
后面补上。
6. 引用
- 概念
引用就是给一个变量取别名,既然是别名,那么和变量名一样都是指这个变量,所以引用变量和变量共用一块内存空间。
int main()
{
int a = 0;
int& b = a;//b是a的别名
cout << &a << endl;
cout << &b << endl;
return 0;
}
打印结果
- 特点
//1.引用在定义的时候必须初始化
int main()
{
int a = 10;
int& b;//错误的
int& b = a;
}
//2. 一个变量可以有多个引用
int main()
{
int a = 0;
int &b = a;
int &c = b;
int &d = a;
//如果b++,c++,d++,那么a也会++。因为他们都是共用同一块空间。
}
//3. 引用一旦引用其他变量,就不能再引用其他变量
int main()
{
int a = 10;
int&b = a;
int c = 20;
b = c;//这里是将d变成c的引用?还是将c赋值给d?
//答案是将c的值赋值给b
}
- 应用
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = x;
}
int main()
{
int x = 10;
int y = 20;
Swap(x, y);
return 0;
}
形参是实参的别名(形参和实参共用一块内存空间),对形参的改变就是对实参的改变
- 适用场合
//1.做参数
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = x;
}
//好处:(1)形参的改变引起实参的改变;(2)提高效率,对于一些比较大的参数
//2.做返回值
int count()
{
int n = 0;
n++;
return n;
}
//n返回时会生成临时变量,再返回。原因是变量出了作用域就销毁。
//如果返回的是静态变量呢?
int count()
{
static int n = 0;
n++;
return n;
}
//此时变量出了作用域就不销毁了,但还是会临时变量。
//不管是局部变量还是临时变量,如果是传值返回,都会生成临时变量。
//那怎样才能不生成临时变量?不生成临时变量有什么用?
//(1)使用传引用返回,就不会生成临时变量。(2)减少拷贝,提高效率。如果返回是大对象,效率会提高得更明显。
//此时引用做返回值的好处就体现出来:减少拷贝,提高效率。
int& count()
{
static int n = 0;
n++;
return n;
}
//疑问:如果count中的n是局部变量,不是静态变量,还可以用引用返回码?
//答案:不可以。局部变量出了作用域就销毁了,会非法访问,结果不可知。如果函数结束,函数栈帧销毁,没有清理栈帧,那么n的值侥幸是正确的。
int main()
{
int& ret = count();
return 0;
}
总结
(1)基本任何形参都可以用引用传参。
(2)谨慎引用做返回值。出了函数作用于,对象不在就不能引用返回,还在就可以用引用返回。正确的样例:静态变量,全局变量。
- 常引用(重点)
//常引用
int main()
{
const int a = 10;
int& b = a;//错误的。引用过程中,权限不能放大。a不能改变,却可以通过b来改变a,这是错误的。
const int c = 10;
int d = c;//正确的。这是一份拷贝,d的值的改变不影响c的值。
int x = 0;
int& y = x;//正确的。权限的平移。
const int& z = x;//正确的。权限的缩小。引用过程中,权限可以平移或者缩小
++x;//正确的。//缩小的是z作为别名的权限,x的权限不缩小。
//此时z是否会发生变化?z也会发生变化,不能通过z修改x,但x改变,z是x的别名,也会随之改变。
const int& m = 10;//正确的。因为常量具有常属性,不可以通过m改变10。
double e = 1.11;
int ii = e;//正确的。这是一份拷贝。但在拷贝过程中有类型转换,此时会产生临时变量。
int& iii = e;//错误的。iii放大了权限,因为临时变量具有常性。e首先产生临时变量,再赋值给iii。
const int& iii = e;//正确的。此时相当于权限的平移。
return 0;
}
常引用还体现函数的返回值
int func1()
{
static int x = 10;
return x;
}
int& func2()
{
static int x = 10;
return x;
}
int main()
{
int& ret1 = func1();//错误的。func1返回的是x的拷贝,是临时变量,具有常属性,此时相当于权限的放大
const int& ret2 = func1();//正确的。权限的平移。
int ret1 = func1();//正确的。返回一份拷贝。
//注意:只有引用时才有权限的放大、缩小和平移。
int& ret3 = func2();//正确的。权限的平移。
const int& ret4 = fun2();//正确的。权限的缩小。
return 0;
}
注意
只有类型转换的时候才会产生临时变量,相同类型不会产生临时变量。
- 引用和指针的区别
(1)相同点
int a = 10;
int& aa = a;//在语法层面上,引用不开空间,是对a取别名
int* pa = &a;//在语法层面上,指针开空间,存储a的地址
但在底层实现上,引用是类似指针的方式实现的。
(2)不同点
- 在概念上,引用定义一个变量的别名,指针存储一个变量的地址。
- 引用在定义式必须初始化,指针没有要求。
- 引用在初始化时引用一个实体后不能再引用其他实体,而指针可以在任何时候指向任何一个实体。
- 没有空引用,但有空指针。(但有野引用和野指针)。
- sizeof(引用)==引用类型的大小,sizeof(指针) ==4/8字节。
- 引用+1,实体就+1;指针+1,就往后偏移一个类型大小的距离。
- 有多级指针,但没有多级引用。
- 访问实体方式不同,指针需要显示解引用,引用编译器自己处理。
- 引用相对于指针而言更安全和便利。
7. auto
- 概念
auto是一个类型指示符,用来自动识别类型。 - 例子
//auto关键字
int main()
{
int a = 10;
auto b = 20;
auto c = 3.14;
auto d = 'H';
cout << typeid(b).name() << endl;//打印类型
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
打印结果
- 注意
(1)使用auto时必须初始化,auto能自动识别类型的本质是在编译阶段,编译器根据初始化表达式推导出变量的实际类型,然后将auto替换。
(2)当auto在同一行定义多个变量时,要保持这些变量必须是相同的类型,否则编译器会报错。原因是编译器只推导第一个变量的类型,然后用推导出的类型定义其他变量。
int main()
{
auto a = 1, b = 2, c = 3;//正确的
auto x = 3.14, y = 4;//错误的
return 0;
}
(3)当auto声明指针类型时,auto*和auto没有区别,但当auto声明引用类型时,就必须带上&。
int a = 1;
auto b = &a;
auto* c = &a;
auto& d = b;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
打印结果
(4)auto不能作为函数的参数
编译器无法对形参的实际类型进行推导
(5)auto不能直接用来声明变量
- 应用
auto通常用来代替复杂的类型,还可以与范围for配合使用。
8. 范围for
- 概念
//范围for
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
for (auto e : arr)
cout << e << " ";
return 0;
}
打印结果
(1)从一个范围确定的数组中取出元素放在e中,然后打印,不管数组中元素的类型。这就是范围for。
(2)它会自动迭代,自动判断结束。
(3)for后面的括号分为两部分,冒号前是循环的变量,冒号后是循环的范围。
(4)这里的e是临时变量的名称,可以随便取。
(5)如果循环过程中要改变数组的内容,就得用引用类型,因为e是arr元素的临时拷贝,不改变arr。
for(auto&e:arr)
e+=1;
- 注意
for循环的范围必须是确定的。对于数组而言,范围就是第一个元素和最后一个元素。
9.nullptr空指针
NULL在C语言被定义为空指针,但在C++被定义为字面常量0。如下面的例子
所以就有了nullptr,它表示空指针。
10.内联函数
- 引入
int Add(int x, int y)
{
return x + y;
}
int main()
{
for (int i = 0; i < 10000; i++)
{
cout << Add(i, i + 1) << endl;
}
return 0;
}
Add循环10000就需要建立10000个函数栈帧,如果要减少建立栈帧的次数,要如何解决?
用宏定义,宏的优点:不需要建立栈帧,提高调用效率。
但考虑到宏的缺点:(1)复杂、容易出错(2)可读性差,不能调试。所以C++推出内联函数。
- 定义
用inline修饰的函数叫做内联函数。编译时C++编译器会在调用内联函数的地方展开,这样就没有函数调用产生的开销,提高运行效率。
inline int Add(int x, int y)//在函数钱前面+inline
{
return x + y;
}
- 疑问
是不是所有的函数都可以用内联函数?
并不是。inline也有适用范围:短小的频繁调用的函数。函数太长会导致代码膨胀。
例子
假设有一个函数Func(),编译后有50条指令。如果有一个项目在10000个位置调用该函数。
(1)第一种情况:Func不是内联函数,合计多少条指令?
项目中每个调用Func的地址都只有一条调用指令call,一共10000+50条指令。
(2)第二种情况:Func是内联函数,合计多少条指令?
Func每次调用都被展开,所以一共10000*50条指令。那么这个可执行程序就变大(例:安装包变大,或者升级程序变大)。
所以并不是所有的函数都可以变成内联函数。
-
inline对于编译器仅仅是个建议,最终是否成为内联函数,由编译器决定。像比较长的函数、递归函数+inline后就会被编译器否决掉。
-
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址,链接就会找不到。
-
默认debug下面,inline不会起作用,否则函数都被展开了,无法进入函数,不方便调试。