第一部分 C++入门
1.命名空间
命名空间实际上就是为了防止命名相同会导致很多的冲突,可以使用命名空间对标识符进行本地化,来避免命名的冲突
使用方式: 1.加命名空间名称及作用域限定符std::cout<<"hello world"<<std::endl; ,特点是比较麻烦,但是它是最规范的方式 2.用using namespace 命名空间名称引入using namespace std; ,把std整个展开,相当于库里的东西都到全局域了,看起来是方便了,但是如果我们自己定义的东西和库里的东西冲突了就没办法解决了。 3.使用using将命名空间中成员引入using std::cout; ,对库里常用的东西进行展开,折中了方法1和方法2。
注: 1.命名空间中的内容可以定义变量也可以定义函数 2.命名空间可以嵌套 3.同一个工程中可以存在多个相同的命名空间,编译器会在最后合成同一个命名空间 4.一个命名空间定义了一个新的作用域,其中的所有的内容都局限于该命名空间中
2.缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。缺省参数的作用就是:调用的时候更加灵活。
缺省参数可以分为: 1.全缺省参数 2.半缺省参数
注: 1.半缺省参数必须从右往左依次来给出,不能间隔着给 2. 缺省参数不能在函数声明和定义中同时出现(如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值) 3.缺省值必须是常量或者全局变量 4.C语言不支持(编译器不支持)
3.函数重载
是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。 int Add(int x,int y); double Add(int x,int y); 这两个函数不是函数重载,它们只是返回值不同。
为什么C++支持函数重载,而C语言不支持函数重载呢? 在C/C++中,一个程序要运行起来,需要经过预处理、编译、汇编、链接四个过程,项目通常是由多个头文件和多个源文件构成。
以Add函数来说,假如Add函数的定义在a.cpp,而在b.cpp中使用了Add函数,当这两个文件分别生成了各自的目标文件a.o,b.o,此时在b.o中是找不到Add函数的地址的,那么经过最后一步链接,b.o就会去a.o的符号表中去找Add函数的地址,然后链接到一起,而此时链接器则用的是经过特殊命名规则修饰过的名字到符号表中去找,而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】,这里只要函数的参数不同,那么在符号表中的函数名就会不同,而C语言就没有这样的一套函数名修饰规则,所以在C语言中尽管函数名相同函数参数不同,在符号表中的函数名是相同的,这是当我们想要调用这一函数时,系统就不知道该去调用哪个函数,所以C语言时不支持函数的重载的。 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
extern"C":用C的函数名修饰规则 有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree()两个接口来使用,但如果是C项目就没办法使用,在C项目中,函数名没有修饰规则,而C++函数有,就导致C语言的编译器无法找到对应接口的经过修饰的函数名,这时可以在函数前加上exter"C",使得该函数按照C的风格来编译。
4.引用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。在语法上,这里是取了一个别名,没有新开空间,实际从汇编实现的角度,引用的底层也是类似指针存地址的方式处理的。
注: 1.引用类型必须和引用实体是同种类型的 2.引用在定义时必须初始化 3.一个变量可以有多个引用 4.引用一旦引用一个实体,再不能引用其他实体
常引用问题: 再谈隐式类型转换: 隐式类型转换转换时不是直接转换,而是先创建一份要转换类型的临时变量,随后赋值给这块临时变量。
假如存在常量 const int a = 10; 我们在对a进行引用时需要加上const,const int& ra = a; 此外,临时变量具有常性,在类型不同的变量间引用时要加上const。 const int& ra = b;
引用使用场景:可以作函数传参或者函数返回值,如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
传值、传引用效率比较:以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。而传引用则避免了这种拷贝,直接将传的参数进行处理。
值和引用的作为返回值类型的性能比较:在传值作为返回值时,并不是直接将return之后的值返回给函数,因为当函数结束后,函数栈帧销毁,这个需要返回的变量也随之销毁,编译器无法知道返回的是什么值,事实上,在传值返回时,编译器会构造一份和返回类型相同的类型然后进行拷贝,返回给函数的是一份临时拷贝;而传引用返回则是返回该类型所在位置的值,没有发生拷贝,只是说,当出了函数作用域,返回对象已经还给系统了,就不能用引用返回了。 当参数和返回值是比较大的变量时,传引用传参和传引用作返回值还可以提高效率,只要是符合条件,尽量使用引用传参、传返回值。
引用和指针的区别: 1.引用在定义时必须初始化,指针没有要求 2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体 3. 没有NULL引用,但有NULL指针 4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节) 5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小 6. 有多级指针,但是没有多级引用 7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理 引用比指针使用起来相对更安全
指针、传值、传址、传引用: 指针:指针就是一个变量,就内存的分布来说,指针和一个变量在内存中存放是没有任何区别的,无非指针存放的是变量的地址。
传值:就是把实参拷贝给形参,是一种单向的传递,赋值完之后实参和形参就没有任何的联系,形参的改变不会影响实参。
传址:实际上传址也是一种传值,因为传址是把实参的地址拷贝给形参,复制完成后实参的地址和形参的地址就没有了任何的联系,对形参地址的改变不会影响到实参的地址,但是对形参地址所指向的对象的修改却直接反映在实参中,因为形参指向的对象就是实参指向的对象。
传引用:传引用的本质就是没有任何的拷贝。两个变量指向同一个对象,形参相当于实参的一个别名,那么对形参的修改必然反映到实参上。
传引用和传指针看上去效果是一样的,到那时本质上有区别:在传指针时,在没有const修饰形参时,形参是可以被改变的,形参的指针可以指向任何的地方,当形参指针改变了,就无法再访问到实参了,这也是传指针为什么要用const修饰的原因。
在引用传递过程中,函数的形参同样作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参的地址。被调函数对形参的任何操作都被处理成通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参的任何操作都影响了实参。
拓展知识: 数组越界是不一定报错的,越界基本不报错,因为编译检查不出来。 越界写,可能会报错,系统查越界使用的抽查。
5.内联函数
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。在类里面定义的函数就默认是内联函数。
如果在函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
inline int max(int x,int y)
{
return x>y?x:y;
}
cout<<max(x,y)<<endl;
以上的一部分代码在编译时展开为:
cout<< (x>y?x:y) <<endl;
从而消除了把 max写成函数的额外执行开销。
上面的例子说明,并不是所有地方都适合用内联函数,指令变多意味着编译出来的可执行程序变大,那么安装软件的人体验变差,执行程序内存消耗变多。
结论:频繁调用小函数(不多于10行代码),建议定义成inline。
注: 1.inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。 2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等(函数太大或者有递归),编译器优化时会忽略掉内联。 3.inline不建议声明和定义分离,分离会导致链接错误(inline函数没有地址)。因为inline被展开,就没有函数地址了,链接就会找不到。一般来说建议在函数实现的地方加上inline。
宏的优缺点: 优点: 1.增强代码的复用性 2.可以提高性能 缺点: 1.不方便调试(预编译期间进行了替换) 2.代码的可读性差,语法复杂容易出错 3.没有类型安全的检查
例如要写一个ADD的宏函数#define ADD(x,y) ((x)+(y)) ,体现语法的复杂。
C++有哪些技术替代宏? 1.常量定义 换用const 2. 函数定义 换用内联函数
6. auto关键字
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。 使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
使用细则: 1.用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
2.当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。 3.auto不能作为函数的参数 4.auto不能直接用来声明数组
第二部分 类和对象(上)
1.类
在C++中,struct 和class 都可以表示一个类,只是它们的区别就是struct 默认的成员是共有的(public),而class 默认的成员是私有的(private)。
class className
{
};
class为定义类的关键字,ClassName为类的名字,{ }中为类的主体,注意类定义结束时后面分号。 类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。 类的两种定义方式:一般情况下,更期望采用第二种方式。 1.声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。 2.声明放在.h文件中,类的定义放在.cpp文件中
访问限定符:C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。 1.public修饰的成员在类外可以直接被访问 2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的) 3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止 4. class的默认访问权限为private,struct为public(因为struct要兼容C) 5. 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
C++中struct和class的区别是什么? C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类,和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是private。
面向对象的三大特性:封装、继承、多态。在类和对象阶段,我们只研究类的封装特性,那什么是封装呢? 将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。我们使用类数据和方法都封装到一下,不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理。
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 : : 作用域解析符指明成员属于哪个类域。
类的实例化: 用类类型创建对象的过程,称为类的实例化。 1.类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它 2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量 3. 实例化出的对象才能实际存储数据,占用物理空间
类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小? 一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类 。只保存成员变量,函数没有存到对象中,成员函数存放在公共的代码段。
class A1
{
public:
void f1(){}
private:
int _a;
};
class A2
{
public:
void f2(){};
};
class A3
{};
A2,A3的大小都是1字节,不是为了存储数据,是占位,表示对象存在过。
复习:结构体内存对齐规则 1.第一个成员在与结构体偏移量为0的地址处。 2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 3. 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的对齐数为8,可以#pragma pack() 来修改默认对齐数。 4.结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。 5.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
1. 为什么要进行内存对齐 实际上是一种空间换取时间的做法,系统在内存中取数据时是一块一块取的,假如在系统取的四个字节中包含了一个char类型和一个int类型的前3个字节,那么系统还得进行第二次获取,降低了效率,可以理解为把数据放到系统方便取到的地址空间中 其次,由于各个编译平台的不同,并不是所有的硬件平台都能访问任意字节的数据,就会导致平台移植性的问题。 2. 如何让结构体按照指定的对齐参数进行对齐
#pragma pack(N)
3. 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景 大端(存储)模式:是指一个数据的低位字节序的内容放在高地址处,高位字节序存的内容放在低地址处。 小端(存储)模式:是指一个数据的低位字节序内容存放在低地址处,高位字节序的内容存放在高地址处。
int main()
{
int i = 1;
char ret = *((char*)&i);
}
若ret的值为1,则为小端存储,否则为大端存储。
union
{
int i;
char c;
}un;
int main()
{
un.i = 1;
printf("%d\n",un.c);
return 0;
}
若un.c为1,则为小端存储,否则为大端存储。
2. this指针
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
哪个对象去调用成员函数,成员函数中访问的就是哪个对象中的成员变量,是通过this指针做到的。
特性: 1.this指针的类型:类类型* const 2. 只能在“成员函数”的内部使用 3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。 4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
|