从这篇博客开始,我将进入对C++的又一次学习。正如名字一样,C++是C的升级和扩充,C++兼容了C的几乎所有语法,在C的基础上增加了许多有用的库,并且增加了面向对象编程的思想。C++是在C的基础上创造出的新的语言,学习C++就必须要搞清楚从C到C++有哪些保留和改变,首先得明白基础语法的区别。这篇博客从基础语法的角度,梳理从C到C++的语法的变化。
命名空间
命名冲突问题
1. 自己定义的变量、函数名很容易和库里面的名字重复
2. 项目比较大时,容易和别人的代码之间命名冲突
C语言没法解决这个问题,C++提出了新的语法——命名空间namespace 简单来说,命名空间是为了避免命名冲突或者名字污染。创建一个命名空间,就是创建了一个作用域,出了作用域命名空间中内容都不可以使用,同样的,我们要使用命名空间中的内容,直接去命名空间中找就行。如此,我们定义的名字跟库中哪怕重复了,也不会相互影响。
#include<stdio.h>
#include<stdlib.h>
namespace ZWK
{
int rand = 10;
}
int a = 0;
int main()
{
printf("Hello, world!\n");
printf("%d\n", rand);
printf("%d\n", ZWK::rand);
int a = 1;
printf("%d\n", a);
printf("%d\n", ::a);
return 0;
}
- 命名空间中可以定义变量/函数/类型
namespace ZWK
{
int a = 0;
int b = 1;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
printf("%d\n", ZWK::a);
struct ZWK::Node node;
ZWK::Add(1, 2);
return 0;
}
- 命名空间可以嵌套定义
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N2
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
int main()
{
int a = 10, b = 20;
int c = N1::Add(a, b);
int d = N1::Add(N1::a, N1::b);
int e = N1::N2::Sub(a, b);
printf("%d %d %d", c, d, e);
return 0;
}
- 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
命名空间的使用 命名空间有三种使用方式(用ZWK命名空间举例)
- 加命名空间或者作用域限定符
ZWK::a - 展开命名空间中常用的变量
using ZWK::Node - 全部展开。用起来方便,但是隔离失效了 最好别用
using namespace ZWK
具体见下方代码:
namespace ZWK
{
int a = 0;
int b = 1;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
using ZWK::Node;
int main()
{
printf("%d", ZWK::a);
Node node;
node.val = 1;
printf("%d", node.val);
return 0;
}
C++输入&输出
在C语言中,我们的输入、输出一般是使用scanf 和printf ,在使用之前我们会包含它们的头文件stdio.h 。 在C++中引入了新的输入、输出方式:cin 和cout ,例如,输入变量a的方式为cin >> a; ,输出变量a的方式为:cout << a << endl; 。其中,endl是换行的意思,>> 是流提取符号,<< 是流插入符号。同样的我们在使用它们之前需要包含它们的头文件iostream 。与C语言有所区别的是,因为C++中引入了命名空间规则,cin、cout 写在命名空间std中,因此在使用他们之前需要展开。上面我们说了,命名空间展开有三种方式,针对我们要使用的cin、cout 会有以下三种具体使用方法:
- 使用作用域限定符。
std::cout << a; ,std::cin >> a; - 单独展开
using std::cout; using std::cin; using std::endl; - 全部展开
using namespace std;
在日常练习C++时,为了方便,我们直接使用的三种方式就行,对性能影响不大。
#include<iostream>
using std::cout;
using std::endl;
using std::cin;
int main()
{
cout << "Hello World!" << endl;
cout << "Hello World!\n";
cout << "Hello World!"<<'\n';
int i = 10;
double d = 1.11;
cout << i << " hhh " << d << endl;
cin >> i >> d;
cout << i << " hhh " << d << endl;
return 0;
}
如何选择输出方式? 怎么方便怎么来,printf和cout都可以用
struct Stu
{
char name[10];
int age;
};
int main()
{
const char* str = "hello";
cout << str << endl;
printf("hello\n");
Stu s;
cin >> s.name >> s.age;
cout << "姓名:" << s.name << endl;
cout << "年龄:" << s.age << endl;
printf("姓名:%s\n年龄:%d", s.name, s.age);
return 0;
}
参数缺省
函数定义的时候,给形参一个默认值,这样在调用函数的时候就可以不输形参
void Func_1(int a = 0)
{
cout << a << endl << endl;
}
void Func_2(int a = 10, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl << endl;
}
void Func_3(int a, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl << endl;
}
int main()
{
Func_1(10);
Func_1();
Func_2();
Func_2(1,2);
Func_3(1);
return 0;
}
注意:
- 半缺省参数必须从右往左依次来给出,不能间隔着给
- 缺省参数不能在函数声明和定义中同时出现。要么出现在定义,要么出现在声明,推荐写在声明中
void Func(int a = 10);
void Func(int a = 20)
{}
- 缺省值必须是常量或者全局变量
- C语言不支持(编译器不支持)
参数缺省的使用场景
定义栈时使用缺省参数,如果我们知道栈中要存多少数据,那么我们就自己定义容量大小(可以避免反复增容带来的消耗),如果不知道,那就使用默认值。
函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”
函数重载: 是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
只有以下三种情况构成重载
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;
}
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
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(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
注意:参数是否缺省,不影响是否重载
为什么C++可以重载而C语言不能重载?
C语言在编译的时候,两个重载函数的函数名相同,在.o 中的符号表中存在歧义和冲突,其次链接的时候也存在歧义和冲突,因为他们都是使用函数名去标识和查找,而重载函数的函数名相同。
而C++的目标文件符号表中不是直接用函数名来标识和查找函数。C++具有函数名修饰规则,即重载函数在.o 中的符号表中生成的指令是不同的,这样就不存在歧义了。链接过程中调用重载函数时,通过地址来寻找函数时也是明确的。
引用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。 比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"
引用定义
int main()
{
int a = 10;
int& b = a;
int* p = &b;
b = 30;
return 0;
}
引用语法
int main()
{
int a = 10;
int& b = a;
int aa = 10;
int& bb = aa;
int& cc = aa;
int& dd = cc;
int aaa = 10;
int& bbb = aaa;
int c = 20;
bbb = c;
cout << bbb << endl;
return 0;
}
引用的作用
- 引用做参数 - a.提高效率,减少拷贝带来的消耗 b.形参的修改可以影响到实参
void swap(int& p1, int& p2)
{
int tmp = p1;
p1 = p2;
p2 = tmp;
}
void swap(int r1, int r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
void swap(int* r1, int* r2)
{
int tmp = *r1;
*r1 = *r2;
*r2 = tmp;
}
int main()
{
int x = 0, y = 1;
swap(x, y);
cout << x << " " << y << endl;
swap(&x, &y);
cout << x << " " << y << endl;
return 0;
}
注意:在另一个作用域中,可以给实参起跟实参相同的别名,因为是处于不同栈帧的。
int main()
{
int a = 10;
int* p = &a;
int*& q = p;
cout << a << " " << *p << " " << *q << endl;
return 0;
}
- 引用做返回值 - a、提高效率 b、修改返回对象,实现可读可写
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
int Add_1(int a, int b)
{
int c = a + b;
return c;
}
int& Add_2(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int ret = Add_2(1, 2);
cout << ret << endl;
return 0;
}
返回值为引用举例
int& func(int a, int b)
{
int* p = (int*)malloc(sizeof(int) * 1);
*p = a + b;
return *p;
}
int main()
{
int a = 10;
int b = 20;
int c = func(a, b);
cout << c << endl;
return 0;
}
可读可写举例
int& at(int i)
{
static int a[10];
return a[i];
}
int main()
{
for (int i = 0; i < 10; i++)
{
at(i) = 10 + i;
}
for (int i = 0; i < 10; i++)
{
cout << at(i) << " ";
}
cout << endl;
return 0;
}
- 引用传值
#include <time.h>
struct A
{
int a[10000];
};
void TestFunc1(A a)
{}
void TestFunc2(A& a)
{}
void TestRefAndValue()
{
A a;
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
常引用
int main()
{
const int a = 10;
const int& b = a;
int c = 10;
const int& d = c;
double d = 11.11;
const int& d2 = d;
return 0;
}
注意
指针和引用的区别
- 引用在概念上就是一个变量的别名 vs 指针存储一个变量的地址
- 引用在定义时必须初始化 vs 指针最好初始化,但是不初始化也没错(也可以编译过)
- 引用定义是初始化引用一个实体后不能再引用其他实体 vs 指针可以任意指向同类的一个实体。(引用从一而终,指针像极了渣男)
- 没有NULL引用 vs 有NULL指针
- 在sizeof中:引用的结果为引用类型的大小 vs 指针的大小始终为4个字节(32)或者8个字节(64)
- 引用自加是引用的实体自加 vs 指针自加是向后偏移一个所指向类型大小的位置
- 引用比指针安全一点,指针的使用更危险。
引用与指针
int main()
{
int a = 10;
int& b = a;
int* p = &a;
b = 2;
*p = 3;
cout << a << endl;
cout << b << endl;
cout << *p << endl;
cout << &a << endl;
cout << &b << endl;
cout << p << endl;
return 0;
}
内联函数
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
内联函数的作用
不使用内联函数时: 如果多次调用Add函数,就需要多次开辟栈帧,消耗巨大。
使用内联函数时: 如果使用了内联函数,编译器会将Add函数在使用的地方展开,不需要开辟栈帧。
有了内联就可以不使用宏,使用安全性较高(指不容易出错)。
内联函数的特性
-
inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。 -
inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。 -
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
auto关键字(C++11)
auto简介
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得 。
总结来说:随着程序越来越复杂,程序中使用的类型越来越复杂,类型名很长,写起来很麻烦,于是有了auto根据变量来自动推导类型。
注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型 。
auto可以自动推导类型
int main()
{
const int a = 0;
int b = 0;
auto c = a;
auto d = 'A';
auto e = 11.11;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
cout << typeid(e).name() << endl;
std::map<std::string, std::string> dict = { {"sort", "排序"} ,{"insert" , "插入"} };
auto it = dict.begin();
return 0;
}
auto使用细则
- auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0;
}
auto不能做的事情
-
auto不能作为函数的参数
void TestAuto(auto a)
{}
-
auto不能直接用来声明数组 void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
-
为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法 -
auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用
语法糖 - 范围for
int main()
{
int array[] = { 1,2,3,4,5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
{
cout << array[i] << " ";
}
cout << endl;
for (auto& e : array)
{
e++;
}
for (auto e : array)
{
cout << e << " ";
}
return 0;
}
注意:
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
指针空值nullptr(C++11)
C++使用nullptr的原因: 应对极端情况下的函数重载,见下面代码:
void f1(int x)
{
cout << "f(1)" << endl;
}
void f1(int* x)
{
cout << "f(1)" << endl;
}
int main()
{
int* p1 = NULL;
int* p2 = 0;
f1(0);
f1(NULL);
int* p3 = nullptr;
return 0;
}
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入
的。 - 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
|