C语言之父—丹尼斯·里奇
C++之父----本贾尼·斯特劳斯特卢普
几个关键时期
C++98
:C++标准第一个版本,以模板方式重写C++标准库,引入了STLC++11
:增加了若干特性,让C++更像一种新语言C++98 下共计有63个关键字,其中有32个关键字是C89 C90标准下的关键字
表示一个标识符的可见范围
使用namespace{}
的形式定义
e.g
namespace N1 { int a; int Add(int left, int right) { return left + right; } } namespace N2 { int a; int b; int Add(int left, int right) { return left + right; } namespace N3 { int c; int d; int sub(int left, int right) { return left - right; } } } namespace N1 { int Mul(int left, int right) { return left * right; } }
注意:
namespace N { int a = 10; int b = 20; int Add(int left, int right) { return left + right; } } int main() { //1、命名空间及作用域限定符 printf("%d\n",N::a); return 0; }
namespace N { int a = 10; int b = 20; int Add(int left, int right) { return left + right; } } using N::b; int main() { //1、命名空间及作用域限定符 //printf("%d\n",N::a); //2、使用using将空间中的成员变量引入 printf("%d\n",b); return 0; }
namespace N { int a = 10; int b = 20; int Add(int left, int right) { return left + right; } } //using N::b; using namespace N; int main() { //1、命名空间及作用域限定符 //printf("%d\n",N::a); //2、使用using将空间中的成员变量引入 //printf("%d\n",b); //3、使用using namespace将命名空间名称引入 printf("%d\n",Add(10,20)); return 0; }
总结
using namespace std; int main() { cout << "Hello World!" << endl; return 0; }
声明或定义函数时为函数的参数指定一个默认值。在调用该函数时。如果没有指定实参则采用该默认值,否则使用指定的实参
e.g
#include <iostream> using namespace std; void testFun(int a = 100) { cout << a << endl; } int main() { testFun(1); testFun(); return 0; }
顾名思义,就是说函数的所有参数都给定了一个默认值
千万别顾名思义,这里的半缺省并不是指一半参数,而是说参数中有部分参数给定了默认值,并且是从右向左依次规定默认值
在同一作用域
中有几个功能类似的同名函数
,他们的形参列表不同
,这样的一组函数称为函数重载
两同一不同
问题:
要解决这个问题,首先我们需要明确:
在C/C++中,一个程序要运行起来,需要经历预处理 、编译 、汇编、链接四个阶段
预处理阶段主要做以下工作
编译阶段则是将预处理结束后的代码转换为汇编程序
汇编阶段是将上一步生成的汇编程序转换为机器语言即二进制代码
链接则是将生成的二进制代码与库函数以及其他目标文件通过链接器,最终生成可执行程序
在链接过程中,遇到函数调用时会通过查看符号表到对应的库或者是其他声明该函数的地方去找。
而每个编译都有自己的函数名修饰规则。我们可以在Linux平台下通过gcc与g++的编译命令来查看具体的函数名修饰规则
Linux下的编译
C语言代码
通过gcc编译后查看汇编代码
C++代码
结论:
Windows下
的名字修饰规则
和Linux相比,Windows下C++编译器对函数名字的修饰非常难以理解,但是目的都是一样的。
总结
有时候在C++工程中可能需要将某些函数按照C的风格来编译。在函数前加extern "C"是告诉编译器,将该函数按照C语言风格来编译。
e.g
tcmalloc是Google用C++实现的一个项目,他提供tcmalloc和tcfree两个接口来使用,但是如果是C项目就无法使用这两个接口,因此使用extern "C"就可以解决。
几点注意事项:
引用不是定义一个新的变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型&引用变量名 = 引用实体
void TestRef() { int a = 10; int& ra = a; //打印两个变量的内容 cout << a << endl; cout << ra << endl; //打印两个变量的地址 cout << &a << endl; cout << &ra << endl; }
引用在定义时必须初始化
一个变量可以有多个引用
引用一旦引用一个实体,再不能引用其他实体
①执行ra = x;之前
②执行ra = x;之后
void TestRef() { const int a = 10; //ra引用a属于权限的放大,将一个原本只读的变量改为可读可写,所以编译器报错 //int& ra = a; const int& ra = a;//ok! int b = 100; int& rb = b; const int&rrb = b;//rrb引用b属于权限的缩小,因此可以通过编译 int c = 10; double d = 3.14; //将c赋值给d,发生隐式类型转换 //d = c; //double&rc = c;//编译报错 const double& rc = c; }
在该过程中会产生一个临时变量,c先将它的值存在该临时变量中,d取的是临时变量里面double类型的值,而非c自身。
为什么加上const后编译器就不会报错呢?
原因是 在该过程中也会产生一个double类型的临时变量,临时变量具有常性,因此加上const后就不会报错
注意:
void Swap(int& left, int& right) { int tmp = left; left = right; right = tmp; }
int& Count() { static int n = 0; n++; return n; }
结果如下图
那么,下面的代码输出结果是什么,为甚?
int& Add(int a, int b) { int c = a + b; return c; } int main() { int& ret = Add(1, 2); Add(3, 4); cout << "Add(1, 2)is:" << ret << endl; return 0; }
输出结果如下图
原因如下
注
在函数返回时,出了函数作用域,如果返回对象未归还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回
以值作为参数或者返回值类型,在传参和返回值期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
#include<time.h> class 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 < 1000; i++) { TestFunc1(a); } size_t end1 = clock(); //以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 1000; i++) { TestFunc2(a); } size_t end2 = clock(); cout << "TestFunc1(A)-time is " << end1 - begin1 << endl; cout << "TestFunc2(A&)-time is " << end2 - begin2 << endl; } int main() { TestRefAndValue(); return 0; }
#include<time.h> class A { int a[10000]; }; A a; A TestFunc1() { return a; } A& TestFunc2() { return a; } void TestRefAndValue() { //以值作为函数返回值 size_t begin1 = clock(); for (size_t i = 0; i < 100000; i++) { TestFunc1(); } size_t end1 = clock(); //以引用作为函数返回值 size_t begin2 = clock(); for (size_t i = 0; i < 100000; i++) { TestFunc2(); } size_t end2 = clock(); cout << "TestFunc1(A)-time is " << end1 - begin1 << endl; cout << "TestFunc2(A&)-time is " << end2 - begin2 << endl; } int main() { TestRefAndValue(); return 0; }
由上述结果可以看出,传值和引用在作为传参以及返回值类型上效率相差很大
在语法概念上,引用就是一个别名,没有独立空间,和其引用实体共用同一块空间
int main() { int a = 10; int& ra = a; cout << "&a = " << &a << endl; cout << "&ra = " << &ra << endl; return 0; }
在底层实现上其实是有空间的,因为引用是按照指针方式来实现的
int main() { int a = 10; int& ra = a; ra = 20; int* pa = &a; *pa = 20; return 0; }
下面我们来对比一下引用和指针的汇编代码
引用和指针的不同点
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序的运行效率
如果在上述函数前加上inline关键字将其改为内联函数,在编译期间编译器会用函数体替换函数的调用。
查看方式
宏的优缺点
优点
缺点
C++有哪些技术可以替代宏
在早期C\C++中auto的含义是:使用auto修饰的变量,是具有自动存储的局部变量
C++11中,标准委员会赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
int TestAuto() { return 10; } int main() { int a = 10; auto b = a; auto c = 'a'; auto d = TestAuto(); cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; cout << typeid(d).name() << endl; return 0; }
【注】
使用auto定义变量的时候必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导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 = 40; c = 60; return 0; }
void TestAuto() { auto a = 1, b = 2; auto c = 3, d = 4.0;//报错,前后类型不一样 }
//此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导 void TestAuto(auto a) { }
void TestAuto() { int a[] = {1,2,3}; auto b[] = { 2, 3, 4 }; }
在C++98中要遍历一个数组,可以按照以下方式进行
void TestFor() { int array[] = {1,2,3,4,5}; for(int i=0;i<sizeof(array)/sizeof(array[0]);i++) { array[i]*=2; } for(int* p = array;p<array+sizeof(array)/sizeof(array[0]);p++) { cout << *p << endl; } }
对于一个有范围的集合而言,由程序员说明循环的范围是多余的,有时候还容易犯错误。因此C++11中引入了基于范围的for循环for循环后的括号由‘:’分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围
void TestFor() { int array[] = {1,2,3,4,5}; for (auto& e : array) { e *= 2; } for (auto e : array) { cout << e << ' '; } cout << endl; }
for循环的迭代范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围。对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围
以下代码就有问题,因为for的范围不确定
void TestFor(int array[]) { for(auto& e:array) { cout<< e << endl; } }
C++98中的指针空值
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr() { int* p1 = NULL; int* p2 = 0; // …… }
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如
void f(int) { cout<<"f(int)"<<endl; } void f(int*) { cout<<"f(int*)"<<endl; } int main() { f(0); f(NULL); f((int*)NULL); return 0; }
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
注意: