文章目录
目录
3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中
什么是c++
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(objectoriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计
对于面向对象和面向过程的理解(简单理解):
1、什么是面向过程?
简单理解就是:我们去做一件事情,从大到小,从里到外,我们注重的是做这个事情时候每个步骤的细节。
2,什么是面向对象?
就是在过程中出现的人或者物,都可以是一个独立的对象,他们的动作和特征就是对象的属性和方法。
3,面向过程与面向对象的优缺点
(1)面向过程:
- 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源。
- 缺点:没有面向对象易维护、易复用、易扩展
(2)面向对象:
- 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
- 缺点:性能比面向过程低
命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的
命名空间的定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
注意:一个命名空间就定义了一个新的作用域,命名空间中所有的内容都局限于该命名空间中。
1.命名空间中的普通定义
namespace nn
{
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
}
2.命名空间可以嵌套
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;
}
}
}
3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中
命名空间的使用
namespace N
{
int a = 0;
int b = 1;
}
int main()
{
// 编译报错:error C2065: “a”: 未声明的标识符
printf("%d\n", a);
return 0;
}
出错的原因就是没有加命名空间的作用域限定符
正确写法:
int main()
{
printf("%d\n", N::a);
return 0;
}符号“::”在C++中叫做作用域限定符,我们通过“命名空间名称::命名空间成员”便可以访问到命名空间中相应的成员
命名空间的使用有三种方式:
1.加命名空间名称及作用域限定符
int main()
{
printf("%d\n", N::a);
return 0;
}
2.使用using将命名空间中某个成员引入
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
3.使用using namespace 命名空间名称引入
using namespce N;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
C++输入&输出
不同于c语言中输入和输出,c++有自己新的输入输出语言。
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
// 可以自动识别变量的类型
cin>>a;
cin>>b>>c;
cout<<a<<endl;
cout<<b<<" "<<c<<endl;
return 0;
}
说明:
1. 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
2. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。
3. <<是流插入运算符,>>是流提取运算符。
4. 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。
缺省参数
什么是缺省参数:缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
void Func(int a = 0)
{
cout<<a<<endl;
}
int main()
{
Func(); // 没有传参时,使用参数的默认值
Func(10); // 传参时,使用指定的实参
return 0;
}
调用func()时
调用func(10)时
上面就表示缺省参数;
缺省参数的分类
全缺省参数,即函数的全部形参都设置为缺省参数。
include<iostream>
using namespace std;
void func(int a = 0, int b = 1, int c = 2)
{
cout <<"a="<< a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
func();
return 0;
}
半缺省参数:部分参数带有默认值
void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
int main()
{
//至少要传递一个参数 ,因为第一个形参没有默认值
func(10);
return 0;
}
注意:
a和b有默认值,c没有默认值----》编译报错
a和c有默认值,b没有默认值----》编译报错
b和c有默认值,a没有默认值----》编译成功
但是:缺省参数只能从右往左依次给出
函数重载
重载是什么意思?类似于现实生活中的多义词,一个词语有多种含义,但具体是哪种意思,需要结合具体的应用场景
函数重载的概念:在相同的作用域中,多个函数的函数名相同,参数列表不同,与返回值类型是否相同无关
int Add(int x int y)
{
return x+y;
}
int Add(double x double y)
{
return x+y;
}
int main()
{
cout<<Add(1,2)<<endl;
cout<<Add(1.2,2.3)<<endl;
return 0;
}
Add方法的定义有多个,在调用时编译器如何知道调用那个方法的函数重载?
函数重载调用原理:
编译器编译代码时,会对传递实参的类型进行推演,根据推演的结果选择合适的函数进行调用。
出现其它情况时(例如):
int Add(int x int y)
{
return x+y;
}
int Add(double x double y)
{
return x+y;
}
int main()
{
cout<<Add(1,2.1)<<endl;
return 0;
}
上述情况中,出现了没有参数类型对应的函数,编译器会尝试对实参进行隐式类型转换,如果转换之后有类型匹配的函数则调用,否则报错
什么是参数列表不同,什么是与返回值类型是否相同无关?
参数列表不同的体现:参数个数不同,参数类型不同, 类型次序不同
void func()
{}
void func(int a)
{}
void func(double a)
{}
void func(int a, double b)
{}
void func(double a, int b)
{}
如果两个方法仅仅是因为返回值类型不同,没有办法构成重载
C++底层是如何支持函数重载的?
结论:c++编译器将参数的类型编译到最终的名字中
为什么c语言不支持?
c语言的编译器对函数名字的修改仅仅是在函数名之前增加了一个下划线
所以c语言不能支持函数的重载
c++:编译器对函数名字的修饰规则:
通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
注意:如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
引用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
如下图所示:
ra就是给已经存在的a取了一个别名
注意:引用类型必须和引用实体是同种类型的
这时候改变d的值,就会发现rd也发生了改变!(因为它们共用同一个内存空间)
引用的特性
1. 引用在定义时必须初始化
2. 一个变量可以有多个引用(类似于一个人可以有多个绰号)
3. 引用一旦引用一个实体,再不能引用其他实体
常引用
将const类型的引用称为const引用
存在的一些特殊情况:
为什么加了const才可以,编译器发现double和int之间可以发生隐式类型转换,于是重新创建了一块整形的空间,将d中整形部分都放到临时空间中。
因为临时空间是编译器在栈上开辟的,用户既不知道这块空间的名字也不知道这块空间的地址,自然临时空间中的值就不能被修改,即临时空间具有常性。
引用的使用场景
1.为了简化代码直接给一个复杂的表达式取别名
2.用引用作为函数的形参
void Swap(int& c, int& d) { int temp = c; c = d; d = temp; } int main() { int a = 10; int b = 20; Swap(a, b); cout << a << " " << b << endl; return 0; }
在调用时,形参ra是a的别名 rb是b的别名
在swap方法中操作形参时,实际就是在操作实参a和b
注意:如果不想通过形参修改外部的实参,可以将形参设置为const类型的引用
3.用引用作为函数的返回值
在弄清上面的问题,我们必须要了解的前提知识:
1.每个函数在运行时,系统负责给函数创建一个栈帧
2.当函数运行结束之后,栈帧就被销毁了
3.栈帧:存放函数运行时的形参,函数体内部定义的局部变量,以及一些寄存器信息和函数中语句执行时候的中间临时变量等
4.栈帧使用esp和ebp两个寄存器标记的 esp标记栈顶 ebp标记栈底
首先函数调用约定:c/c++中函数默认的调用约定就是_cdecl约定参数从右往左传递,由被调函数清理栈空间。
由上图可知,先压入2入栈然后在压入1,然后给add方法开辟了一个空间,add方法开始执行,执行完了之后将数据保存到了临时空间temp中。因为我们是用引用的方式进行返回,所以不用创建临时变量,结束之后就会将上面的栈帧销毁。然后开始调用下一条指令。
(2)另外一种解释
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
具体理解请观看该博客浅析函数的调用过程_c语言函数不调用_xy294636185的博客-CSDN博客
那怎么可以引用来作为函数的返回值:
只要函数结束后,返回的实体没有随着函数的结束而销毁就可以,比如全局变量,static修饰的变量,堆空间等...
传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低.
传地址和传引用的效率差不多
而引用比指针更安全,代码可读性高,所以一般情况下更推荐传引用
指针和引用区别?
相同点:
传指针和传引用的效率差不多,而且传地址和传引用都可以通过修改形参来改变外部的实参。
由上图可知:
1.指针和引用生成的汇编代码竟然是一样的
2.引用实际在底层也是有空间的,内部实际存放的是其引用实体的地址,代码完全相同说明:在底层引用就是按照指针的方式实现的。
问题:
刚开始接触引用:引用就是别名,编译器不会给引用变量重新定义开辟内存空间,引用与其引用的实体共用一份内存空间。但是发现:引用实际是有空间的,空间中放的是其引用实体的地址。
语法概念:引用就是别名,编译器不会给引用变量开辟空间---》这样方便用户理解
底层实现:要实现引用的技术,底层又把引用换成指针--->引用就是语法层面的概念,在底层实际就是没有引用概念
引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全
引用和指针的常见问题(例题)
void Swap(int& left ,int& right ) { int temp = left; left = right; right = temp; } int main() { int a = 10; int b = 20; Swap(a,b); cout<<a<<""<<b<<endl; int* pa = &a; int* pb = NULL; Swap(*pa ,*pb); return 0; }
对于上面的代码最终可以正常运行吗? 如果崩溃了 最终会在什么地方崩溃?
(由于引用在底层就是指针)所以对于上面的代码最终被编译成如下的样子
void Swap(int* const left ,int* const right ) { int temp = *left; *left = *right; *right = temp; } int main() { int a = 10; int b = 20; Swap(&a,&b); cout<<a<<""<<b<<endl; int* pa = &a; int* pb = NULL; Swap(pa ,NULL); //看起来是对*pa解引用 其实就是把pa直接穿给left ,把NULL传给right return 0; }
因此代码会崩溃,崩溃的地方为 *left = *right; 此处对right解引用,由于right为空导致崩溃。
内联函数
c++对c语言中的宏进行了优化
宏常量
//宏常量的优势:
//1.达到一改全改的效果
//2.可以提高程序的可读性#define MAXSIZE 100 int main() { int array[MAXSIZE]; for (int i = 0; i < MAXSIZE; ++i) { array[i] = i * 10; } for (int i = 0; i < MAXSIZE; ++i) { cout << array[i] << endl; } cout << endl; return 0; }
宏常量的缺陷:
宏常量在定义时没有类型,在预处理阶段发生的替换,也就不会进行类型检测
#define PI "3.14"//这里定义错了,导致下面发生错误 int main() { double r = 2.0; cout << PI*r*r << endl; cout << 2 * PI*r << endl; return 0; }
因为宏常量有缺陷,因此在c++中,使用const定义常量取代了宏
connst int MAXSIZE=100; const double PI=3.14; int main() { //注意: //在c++中,被const修饰的变量不在是变量,而是一个常量 //在c语言中,被const修饰的变量不是常量,而是一个不能被修改的变量 int array[MAXSIZE]; double r = 2.0; cout << PI*r*r << endl; return 0; }
const定义的常量也会发生替换,该替换是在编译时
在所有读取a常量中内容的位置,比如打印常量 都会使用常量本身进行替换int main() { const int a = 10; int*pa = (int*)&a; *pa = 100; cout << a << "" << *pa << endl; //在所有读取a常量中内容的位置,比如打印常量 都会使用常量本身进行替换 a被替换成了10 //cout<<10<<<<*pa<<endl; return 0; }
宏函数
优点:在预处理阶段会展开,(展开:就是用宏替换宏使用的位置)少了函数替换的开销
缺陷:
1.不会进行类型检测
2.不能调试
3.有副作用
4.会造成代码膨胀#define Add(x,y) x+y #define MAX(x,y) (x)>(y)?(x):(y) int main() { cout << Add(1, 2) << endl; int a = 10; int b = 20; int ret = MAX(++a, b);//(++a)>(b)?(++a):(b) a就加了两次 cout << ret << endl; return 0; }
内联函数
1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
inline int Add(int x, int y) { return x + y; } int main() { int a =10; int b = 20; int ret = Add(a, b); printf("%d\n", ret); return 0; }
2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
auto关键字(C++11)
c语言中:auto用来定义自动存储类型的局部变量,表明变量可以自动被销毁但是局部变量本来在函数结束之后就可以自动被销毁,显得auto有点多余,没有什么实际的含义
c++11中,auto被赋予了新的含义:用来定义变量的一个占位符,告诉编译器 在编译阶段,推到初始化表达式的类型作为变量真正的类型
int main() { int a=10; double d=12.34; auto a1=10; auto d1=12.34; cout<<typeid(a1).name()<<endl; //int cout<<typeid(d1).name()<<endl; //double return 0; }
auto的使用细则
1.auto和指针与引用结合
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main() { int a=10; auto pa=&a; auto* pb=&a; cout<<typeid(pa).name()<<endl; cout<<typeid(pb).name()<<endl; return 0; }
ra并不是a的别名
注意:在使用auto定义引用类型的变量时,&是不能省略的。
2.在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量void TestAuto() { auto a = 1, b = 2; auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同 }
auto不能推导的场景
1. auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导 void TestAuto(auto a) {}
2. auto不能直接用来声明数组
3. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
4. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用
基于范围的for循环(C++11)
范围for的语法
在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循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围
for(auto e:范围):e将来是范围中的每个元素的拷贝,既不能通过e修改范围中的数据。
for(auto& e:范围):e将来是范围中的每个元素的别名,即可以通过e修改范围中的元素
void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; for(auto& e : array) e *= 2; for(auto e : array) cout << e << " "; return 0; }
范围for的使用条件
1.for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[]) { for(auto& e : array) cout<< e <<endl; }
2. 迭代的对象要实现++和==的操作。
指针空值nullptr(C++11)
在c++11之前,统一使用NULL表示空值指针
int main() { //C++98 int *p1=NULL; //c++11 int *p2=nullptr; return 0; }
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; }
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如上面的代码
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
所以在c++中我们要注意:1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。