书中这么写着:
C语言是结构化和模块化的语言,它是面向过程的。在处理较小规模的程序时,程序员用C语言较为得心应手。但是当问题比较复杂、程序的规模比较大时,结构化程序设计方法就显出它的不足。C程序的设计者必须细致地设计程序中的每一个细节,准确地考虑到程序运行时每时刻发 生的事情,例如各个变量的值是如何变化的,什么时候应该进行哪些输人,在屏幕上应该输出什么等。这对程序员的要求是比较高的,如果面对的是一个复杂问题,程序员往往感到力不从心。当初提出结构化程序设计方法的目的是解决软件设计危机,但是这个目标并未完全实现
为了解决软件设计危机,在20世纪80年代提出了面向对象的程序设计(ObjectOriented Programming,OOP)思想,需要设计出能支持面向对象的程序设计方法的新语言。Smalltalk 就是一种面向对象的语言。而在实践中,人们发现由于C语言是如此深人人心,使用如此广泛,面对程序设计方法的革命,最好的办法不是另外发明种新的语 言去代替它,而是在它原有的基础上加以发展。在这种形势下,C++应运而生。C++是由AT&T Bell(贝尔)实验室的Bjarne Strousrup 博士及其同事于20世纪80年代初在C语言的基础上开发成功的。
C++保留了C语言原有的所有优点,并增加了面向对象的机制。由于C++对c的改进主要体现在增加了适用于面向对象程序设计的“类(clss)。因此最初它被Bjarme Sroustrup称为“带类的C"。后来为了强调它是C的增强版,用了C语言中的自加运算符“++”,改称为C++。
C++是由C发展而来的,与C兼容。用C语言编写的程序基本上可以不加修改地用于C++。从C++名字可以看出它是C的超集。C++既可用于面向过程的结构化程序设计,又可用于面向对象的程序设计,是个功能强 大的混合型的程序设计语言。
C++对C的“增强”,表现在两个方面:
1.在原来面向过程的机制基础上,对C语言的功能做了不少扩充
2.增加了面向对象的机制
面向对象程序设计是开对开发较大规模的程序而提出来的,目的是提高软件开发的效率。
C++为了方便用户,除了可以利用prinf和scanf函数进行输出和输人外,还增加了标准输人输出流cout
和cin
。cout是由c和out两个单词组成的,代表C++的输出流对象,cin是由C和in两个单词组成的,代表C++的输人流对象。它们是在头文件iostream中定义的。键盘和显示器是计算机的标准输人输出设备,所以在键盘和显示器上的输人输出称为标准输人输出,标准流是不需要打开和关闭文件即可直接操作的流式文件。
用cout进行输出
cout必须和输出运算符“<<”起使用。“<<在C语言中是作为位运算中的左移运算符,在C++中对它赋以新的含义:作为输出信息时的“插入运算符”。
cout<<”Hello<<endl;//将字符串“Hello"插入到输出流cout中,也就是说把所指定的信息输出在标准输出设备上。
cout<<a<<b<<c<<endl;
用cin进行输入
从输入设备向内存流动的数据流称为输入流。用cin实现从系统默认的标准输入设各(键盘)向内存流动的数据流称为标准输人流。用“>>”运算符从输人设备键盘取得数据并送到输人流cin中,然后再送到内存。在C++中,这种输入操作称为“ 提取“ 或“得到”。“>>” 常称为“提取运算符”。
int a; //定义整型a
double b; //定义浮点型b
cin>>a>>b; //从键盘接收一个整数和实数
cout<<a<<b<<endl; //输入输出都不需要在指定数据类型了 如整形--%d,浮点数--%f
缺省函数(有默认参数的函数)
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
void TestFunc(int a = 0) //定义缺省函数
{
cout<<a<<endl;
}
int main()
{
TestFunc(); // 没有传参时,使用参数的默认值 0
TestFunc(10); // 传参时,使用指定的实参 10
}
缺省参数分类
1.全缺省参数(参数全给上默认值)
2.半缺省参数(部分,不是说一半的意思)
void TestFunc(int a = 10, int b = 20, int c = 30) //全缺省
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
void TestFunc(int a, int b = 10, int c = 20) //半缺省
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
//a.h
void TestFunc(int a = 10);//头文件声明
// a.c
void TestFunc(int a = 20) //c文件定义
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
注意
1.形参和实参的结合是从左至右顺序的
2. 半缺省参数必须从右往左依次来声明给出,也不能间隔着给。
3. 缺省参数不能在函数声明和定义中同时出现(编译系统会给出”重复指定默认值“的报错信息)
4. 一个函数不能作为重载又作为缺省,调用函数时,无法判定参数的性质会出现二义性。
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表
(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题.
int Add(int left, int right)
{
return left+right;
}
double Add(double left, double right)
{
return left+right;
}
//参数类型不同
short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}
//参数顺序不同
从函数编译链接阶段来解释这个问题。
名字修饰是一种在编译过程中,将函数、变量的名称重新改编的机制,简单来说就是编译器为了区分各个函数,将函数通过某种算法,重新修饰为一个全局唯一的名称。底层调用时就可以直接call这个名字。
C语言的名字修饰规则非常简单,只是在函数名字前面添加了下划线。
比如,对于以下代码,在最后链接时就会出错:
int Add(int left, int right);
int main()
{
Add(1, 2);
return 0;
}
编译器报错:error LNK2019: 无法解析的外部符号 _Add,该符号在函数 _main 中被引用。
上述Add函数只给了声明没有给定义,因此在链接时就会报错,提示:在main函数中引用的Add函数找不到函数体。从报错结果中可以看到,C语言只是简单的在函数名前添加下划线。
结论:因此C语言中当工程中存在相同函数名的函数时,就会产生冲突,无法支持函数重载
而C++修饰规则比较复杂,不同编译器在底层的实现方式可能都有差异。
int Add(int left, int right);
double Add(double left, double right); //函数重载
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
在vs下,对上述代码进行编译链接,最后编译器报错:
error LNK2019: 无法解析的外部符号 "double cdecl Add(double,double)"(Add@@YANNN@Z)
error LNK2019: 无法解析的外部符号 "int __cdecl Add(int,int)" (?Add@@YAHHH@Z)
通过上述错误可以看出,编译器实际在底层使用的不是Add名字,而是被重新修饰过的一个比较复杂的名字,被重新修饰后的名字中包含了:函数的名字以及参数类型。这就是为什么函数重载中几个同名函数要求其参数列表不同的原因。只要参数列表不同,编译器在编译时通过对函数名字进行重新修饰,将参数类型包含在最终的名字中,就可保证名字在底层的全局唯一性。
总结:
1.C语言不支持同名函数,应为链接的时候函数名一样所以区分不开
2.c++支持重载是因为编译后的同名函数修饰名字不同,根据函数名、类名、调用约定、参数类型等共同决定。
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用时压栈(栈帧)的开销,内联函数提升程序运行的效率。
inline int max(int a,int b) //修饰内联函数
{
return a>b? a:b;
}
int main()
{
cout<<max(1,2)<<endl; //会在编译调用的时候 把定义块展开嵌入到程序中,而不是开辟栈帧去调用
}
总结:
1 inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等
等,编译器优化时会忽略掉内联。
3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
4. 内联函数类似于宏函数,但是又有区别:宏函数不能调试,而内联函数可以
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。对一个变量的引用的所有操作,实际上都是对其代表的变量的操作。
基本规则:
1 引用在定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体
注意:引用类型必须和引用实体是同种类型的
void Test()
{
int a = 10;
int& ra = a;//定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra); //地址相同,说明共用一块内存
}
void Test()
{
int a = 10;
// int& ra; // 未初始化 该条语句编译时会出错
int& ra = a;
int& rra = a; //多个引用
printf("%p %p %p\n", &a, &ra, &rra);
}
void Test()
{
int a = 10;
int b = 2;
int& ra = a;
int& ra = b;//错误,ra已是a的引用,不能再声明为b的引用了
}
常引用
引用与const联系之后。
规则:
1.本来是const类型的只读变量,不能被普通变量(可读可写)来引用。理解为权限被放大了(越权)
2.可读可写的普通变量,可以被初始化用来做常引用。理解为权限缩小(管辖之内)
void TestConst()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量 只读
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
//上面的解释如下
//先将double类型的变量转换成int型,存放在临时变量temp中
//temp和rd同类型的,就可以声明引用了
int temp=d;
const int &rd=temp;
}
引用做参数
void Swap(int& left, int& right) //调用时自动变为实参的引用
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int a=1;
int b=2;
swap(a,b);
cout<<a<<b<<endl;
}
引用做返回值
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; //7
return 0;
}
注意:如果函数返回时,离开函数作用域后,其栈上空间已经还给系统,因此不能用栈上的空间作为引用类型返回。
如果以引用类型返回,返回值的生命周期必须不受函数的限制(即比函数生命周期长)。
可以用static定义静态变量
传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,而对于引用来说,并没有产生临时拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;//引用
int* pa = &a;//指针
return 0;
}
反汇编下我们可以看到两者的底层指令是一样的,lea是取变量地址的汇编指令,可见底层实现都是一样的。
引用和指针的不同点:
1 引用在定义时必须初始化,指针没有要求
2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型
实体
3. 没有NULL引用,但有NULL指针
4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
6. 有多级指针,但是没有多级引用
7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
8. 引用比指针使用起来相对更安全
auto简介
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl; //int
cout << typeid(c).name() << endl; //char
cout << typeid(d).name() << endl; //int
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
auto与指针和引用结合
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int x = 10;
auto a = &x;
auto* b = &x; //两者没区别
auto& c = x; //引用必须加&
return 0;
}
注意:
1.auto不能作为函数参数的类型
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
2.auto不能用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6}; //错误
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array) //要想操作数据 要注意加&引用符 e就是对数组里的元素的引用
e *= 2; //每个数乘2的操作 修改
for(auto e : array) //遍历数组 e就是对元素的拷贝
cout << e << " "; //没修改
return 0;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环
int array[] = { 1, 2, 3, 4, 5 };
for (auto& e : array)
e *= 2;
for (auto e : array)
{
if (e == 8)
continue; //或者break
cout << e << " "; //2,4,6,10 或者 2,4,6
}
范围for的使用条件
1.for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin()和end()的方法,begin和end就是for循环迭代的范围。
2.迭代的对象要实现++和==的操作(遍历或判断的条件)。
NULL:
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0 //被定义为 0
#else
#define NULL ((void *)0) //无类型指针(void*)
#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); //必须强转为一个(int*)才可以调用 上面的第二个函数
return 0;
}
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
nullptr
为了考虑兼容性,C++11并没有消除常量0的二义性,C++11给出了全新的nullptr表示空值指针。C++11为什么不在NULL的基础上进行扩展,这是因为NULL以前就是一个宏,而且不同的编译器厂商对于NULL的实现可能不太相同,而且直接扩展NULL,可能会影响以前旧的程序。因此:为了避免混淆,C++11提供了nullptr,即:nullptr代表一个指针空值常量。nullptr是有类型的,其类型为nullptr_t,仅仅可以被隐式转化为指针类型,nullptr_t被定义在头文件中:
typedef decltype(nullptr) nullptr_t;
注意:
1.在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3.为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
动态内存管理,在C语言中是利用库函数malloc与free来分配和撤销内存空间的。但是malloc函数必须要先知道需要指定的空间大小。调用格式为malloc(size),size是字节数,也是要先知道或者用sizeof运算符来求出。而且,malloc的返回值为(void*),必须在程序中强制类型转换,才可以知道返回的指针指向的类型。
而c++提供了较简便而功能强大的运算符new和delete来取代malloc和free函数,但是也保留了这两函数。
new int ;//开辟一个存放整数的空间,返回一个指向整数类型的指针
new int(10); //开辟一个存放整数的空间,并指定整数初值为10
new char[10]; //开辟一个存放字符数组的空间,有十个元素,返回一个指向字符数据的指针
new int[5][6]; //开辟一个存放二维数组的空间,数组大小为 5*6,
float *p=new float(1.12);//开辟一个存放实数的空间,初值为1.12,再将返回的指针赋给指针变量p
char *pc= new char[100];
delete p;//撤销new开辟的存放实数的空间
delete []pc; //撤销数组空间 要在前面加一对方扣号
struct Student
{
...//
}
Student *p=new Student;//开辟一个结构体类型的空间
delete p; //销毁
注意:
1.用new分配数组空间时不能指定初值。
2.由于空间内存不足等原因,分配失败时,new会返回一个空指针NULL。
3.new和delete是运算符,不是函数,所以执行效率高,且两者要配合使用(凑一对)。