算术类型包括了整型(integral type 包括了字符和布尔类型在内)和浮点型。C++:算术数据类型:
各种变量类型在内存中存储值时需要占用的内存,以及该类型的变量所能存储的最大值和最小值,如下所示:
布尔类型(bool)的取值是真(true)或者假(False)
wchar_t、char16_t、char32_t。wchar_t类型用于确保可以存放机器最大扩展字符集中的任意一个字符, 类型char16_t和char32_t, 则是Unicode字符集服务(Unicode是用于表示所有自然语言中字符的标准)C++语言规定, 一个int至少和一个short一样大, 一个long至少和一个int一样大, 一个long long 至少和一个long 一样大。其中long long是在C++11中新定义的。除去布尔类型和扩展字符型外,其他的整型还分为带符号的(signed)和无符号(unsigned)两种,带符号的类型可以表示正数、负数或0, 无符号类型则仅能表示大于等于0的值。
算术类型的值赋值给另外的一个类型时:
C++语言规定的转义序列如下所示:
上述的转义字符只能当一个字符使用。
也可以使用泛化的转义序列。其中的形式是\x后面跟1个或者多个16进制的数字,或者\后面紧跟1个、2个、3个八进制数字, 其中数字部分表示的是字符对应的数值。
C++11新标准的一部分,用花括号来初始化变量得到的全面的运用,在之前, 这种初始化的形式仅在某些受限的场合上运用。
C++的标识符是有子母、数字、下划线组成的,其中子母或下划线开头,标识符的长度没有限制,但是对大小写敏感。
其中C++的关键字不能作为标识符,如上所示:
作用域(scope)是程序的一部分,在其中名字有特定的含义。c++的大部分作用域的用花括号分隔。同一个名字在不同的作用域中可能指向不同的实体。名字的有效区域始于名字的声明语句,也声明语句的所在的作用域未端未为结束。
引用就是别名。引用并非对象,相反的,它只是一个已经存在的对象的另外一个名字。
指针(pointer)是“指向(point to)”另外一种类型的复合类型,与引用类似,指针也实现了对其他对象的间接访问,指针和引用也有很多的不同:
定义指针的方式是:在变量名的前面加上一个*号,例如下面所示:
指针存放某个对象的地址,要获取该地址, 这需要取地址符(&)。
指针的值(地址)应该属于下面的四种情况。
某些符号的多重意义
空指针(null pointer)不指向任何的对象。下面的生成空指针的方法:
建议:初始化所以的指针
void指针是一种特殊的指针,用于存放任何对象的地址。一个void指针存放一个地址,也一点与其他的指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解。
通过 * 的个数来区分指针的级别。也就是说,** 表示指向指针的指针,*** 表示指向指针的指针的指针,依次类推。
有时候, 我们希望定义这样一种变量,它的值不能被该变,在C++中,可以运用关键字const对变量进行限定。例如如下所示:
有时候有这种一种const变量,它的初始值不是应该常量的表达式, 但是却是有必要在文件间共享,我们想让一个文件中定义const,其他的文件中声明并使用它。处理的方式是:对于const变量不管是声明还是定义都添加extern关键字,这样就只需要定义一次就可以了。
当两个或者多个独立编译的源文件中使用了相同的模板并且提供了相同的模板参数时,每个文件中都会有该模板的一个实例。
在大系统中,在多个文件中实例化相同的模板的额外开销可能非常严重,在C++11新标准中,我们可以通过显式实例化来避免这种开销。一个显式实例化具有如下形式:
当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码,将一个实例化声明为extern就表示承诺在程序的其他位置有该实例化的一个非extern定义。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
由于以上讨论可知:extern一般是使用在多文件之间需要共享某些代码时。
可以把引用绑定到const对象上,就像绑定带其他对象上一样,称为对常数的引用(reference to const),不能被用于修改它所绑定的对象。
引用的类型必须与所引用的对象类型相同, 但是两个例外。初始化变量。
与引用一样, 可以令指针指向常量或非常量, 类似常量的引用,指向常量的指针(pointer to const)不能改变其所指对象的值,想要存放常量对象的地址, 只能使用指向常量的指针。
指向常量的指针或引用,觉得自己指向了常量,所以自觉地不去改变所指对象的值。
常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是指针存放的地址)不能改变了,把*放在const关键字之前用也说明指针是一个常量, 也有另外一层的含义,即不变的是指针本身的值并非所指的那个值。
指针本身是不是一个常量以及所指的是不是一个常量就是一个相互独立的问题,用名词顶层const(top-level const)表示指针本身是一个常量, 而用名词底层const(low-level const)表示指针所指的对象是一个变量。
顶层const也可以表示任意的对象是常量,底层const 则与指针和引用等复合类型的基本类型部分有关,指针的类型既可以是顶层const和底层const。
当执行对象拷贝时候,常量是顶层const是底层const和顶层const区别明显,顶层的const不受影响。
一个对象(或表达式)是不是常量表达式是由它的数据类型和初始值共同决定的。
在复杂的系统,很难分辨一个初始值到底是不是常量的表达式。在c++11新标准上,允许变量声明constexpr类型来让编译器来验证变量是值是否是一个常量表达式。声明constexpr的变量一定是一个常量,而且用常量表达式初始化。
类型别名(type alias)是一个名字,它是某种类型的同义词。有两种方式定义类型别名,传统的方式是使用关键字typedef:
类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名:
我们常常需要将表达式的值赋值给变量,这就需要声明变量的时候知道表达式是值的类型。然而有时候, 我们做到这一点我们是不容易的,甚至是不可以的, 在c++11中引用了auto类型的说名符来解决这个问题。在auto定义的时候的变量必须是有初始值的。
如果val1和val2相加的类型是Sales_item类型,这item的类型也是Sales_item类型,如果两个类型是double,这相加的结果也double。
编译器推断出来的auto类型和初始值的类型并不是完全一致的,编译器和适当的改变了结果的类型使符合初始化规则。
一般auto会忽略掉顶层的const,同时底层的const会保留下来,当指向常量的指针。
希望从表达式的类型推断出来要定义的变量和类型,但是不想用该表达式的值初化变量。在C++11引用了decltype,它的作用是返回操作数的数据类型,编译器分析表达式并得到它的类型,并不实际计算表达式值。
decltype处理顶层const和引用的方式与auto方式有些不同,如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内)
decltype((variable))注意双层括号,如果是双层括号,结果永远被引用,而decltype(variable)结果只有当variable本身就是一个引用时才被引用。
C++语言允许用户与类形式定义数据结构,而库类型string 、istream 、ostream都是也类的形式定义的。
Sales_data定义如下所示:
Sales_data也关键字struct开始,后面跟着类名和类体(类体可以为空),类体由花括号围成的新的作用域。其他的方式
不要忘记类定义后面的分号。
头文件通常包括了只能定义的一次的实体,如类、const、constexpr变量等,头文件也经常用到其他头文件的功能。如果在头文件中用到了string成员, 则我们必须包含string.h头文件。头文件一旦改变,相关的源文件必须重新编译也获取更新说明。
确保头文件多次包含仍能安全的工作的常用的技术就是预处理(preprocessor),预处理在编译之前执行一段程序,可以部分改变我们的代码。例如#include, 就是将include 替换为头文件
C++程序还会使用头文件预处理功能,叫作头文件保护符(header),预处理变量的两种状态是:已定义和未定义。#define指令把名字设置为预处理变量, 另外两个指令则分别检查某个指点的预处理变量是否已经定义:#ifdef当且仅当也定义时为真,#ifndef当且仅当变量未定义为真时,一旦检查结果为真,执行后续的操作,直到遇到#endif指令为止。使用这些功能有效的防止重复操作的发生。
在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:stdio.h
string对象的常见的初始化的方式:
如果使用"="号初始化变量,实际上执行的是拷贝初始化(copy initialization),编译器将右边的初始值拷贝到新创建的对象之中,与之相反,如果不使用等号,则就是执行的直接初始化(direct initialization)。
empty是判断string对象是不是为空。size返回的是string对象的长度。
在cctype.h中定义了一些处理字符串的方法
标准库类型vector表示对象的集合,其中所有的对象都是相同的。集合中的每个对象都与一个与之对应的索引,索引用于访问对象,由于vector容纳了所有的对象,称为容器(container)。
vector是模板而非类型,由vector生成的类型必须包含vector中的元素类型。
初始化vector对象的方法
元素访问
我们知道运用下标运算符来访问string对象的字符或vector对象的元素,还有另外一种更加通用的机制可以实现这种目的, 那就是迭代器(iterator)。
和指针不同,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器成员。比如,这些类型都拥有名为begin和end的成员。其中begin成员负责返回指向第一个元素(或第一个字符)的迭代器。
如果迭代器为空,则begin和end返回的都是同一个迭代器,都是尾后迭代器。
标准容器迭代器的运算符
将第一个字母大写
迭代器使用了标准库中iterator和const_iterator来表示迭代的类型。
begin和end返回的具体类型由对象是否为常量决定,如果对象是常量,begin和end返回const_iterator,如果对象为常量,返回iterator:
箭头运算符(->),箭头运算把解引用和成员访问两个操作结合起来,也就是说 it->men 和(*it)->men表达的意思相同。
迭代器的递增运算令迭代器每次移动一个元素,标准库容器都支持递增运算的迭代器。
string和vector迭代器其他的运算符
数组是一种复合类型,数组的形式是a[d],a代表了数组的名,d代表数组的维度,维度表示数组的元素个数,必须大于0, 数组的维度必须是一个常量表达式。
和内置的变量一样,如果在函数内部定义了某种内置的类型的数组,那么默认初始化会令数组含未知值。
和vector一样, 数组能存放大多数类型的对象,例如存放一个指针的数组。数组本身就是对,允许对数组的引用和定义数组指针。
使用地址符&来获取某个对象的指针,适用于如何的对象,对数组取地址符得到该元素的指针。
c语言风格字符串的函数。
当一个数组的元素也是数组时,就表示多维数组。
一个数组表示本身的大小,另外一个数组表示其元素的大小。
数组的定义:
多次嵌套处理数组
在使用for语句处理多维数组,除了最内层的循环外,其他的所以的循环的控制变量都一个是引用类型。
当程序使用多维数组的名字时,也会自动的转换成为指向数组首地址的指针。
在定义指向多维数组的指针时,千万不要忘记了这了这个数组其实是数组的数组。
由于多维数组是数组中的数组,所以有多维数组名转换位来的指针实际是指向第一个内层数组的指针。
使用auto或者decltype就能尽可能的避免在数组前面加一个指针类型了。
sizeof运算符返回一条表达式或一个类型名字所占的字节数,sizeof运算符支持结合律,得到的值是size_t类型(size_t一般用来表示一种计数,比如有多少东西被拷贝等,保证能容纳实现所建立的最大对象的字节大小)。
运算符对象的两种形式:
第二种中,sizeof返回是表达式类型的大小。
类型转换有两种,隐式转换和显示转换。
隐式转换:编译器会自动转换对象的类型。
显示转换:有时候我们不得不使用显示转换,也下面的代码为例,显示转换将两个int的数据相除得到double类型。
虽然有时候我们不得不使用强制转换,本质是危险的。
强制转换类型转换命名习惯cast_name(expression)
cast_name是static_cast、dynamic_cast、const_cast、reinterpret_cast中的一种。
static_cast
任何具有明确定义的类型转换,只要不包括底层的const,都能使用static_cast。
const_cast
const_cast只能改变运算对象底层的const。
reinterpret_cast
reinterpret_cast通常为运算对象的位模式较低层次上的重新解释。
pc所指的真实的对象是一个int而非字符。
表达式语句:是最简单的语句形式,一般格式为:表达式;
空语句:只由一个分号构成的语句,一般格式为: ;
注意:
空语句不执行任何操作,但具有语法作用。例如:for循环在有些情况下,循环体或者循环判别条件是空语句。
从程序结构的紧凑性与合理性角度考虑,尽量不要随便使用空语句。
复合语句:由“{}”括起来的一组语句,一般格式为:
注意:
流程控制语句:用来控制或改变程序的执行方向的语句。
if语句表达式
example:
switch语句
while语句
只要条件为真,while语句 就会重复执行statement,condition不能为空,如果condition第一次求值是false, statement一次都不执行。
定义在while条件部分或者在while循环体在内的变量每次迭代都经历了从创造到被销毁的过程。
使用while循环
当不知道循环多少次数的时候,就可以使用while循环,满足一定的条件,就可以了。
for循环
语法形式
do while
表达式:
break语句
break语句附近的终止它最近的while、do while、for、switch语句。
break语句只能出现的在迭代语句和switch语句之中,break语句的作用范围仅限于最近的循环或者switch:
continue语句
终止本次循环的执行,即跳过当前一次循环中continue语句后尚未执行的语句,然后进行下一次循环条件的判断。
goto语句
goto语句的作用是从goto语句无条件跳转到同一个函数内的另一条语句中。
不要在程序中使用goto语句,使得程序难以理解又难修改。
语法形式
goto 语句一个很好的作用是退出深嵌套例程。
异常是指运行时的反常行为,超过了函数正常功能的范围。
C++异常包括了:
throw表达式
程序的异常检测部分使用throw表达式引发异常,throw表达式包括了关键字throw和紧跟其后的一个表达式,其中表达式的类型就是抛出的异常类型。throw后面紧跟一个分号,从而构成一条表达式语句。
如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:
如果 try 块在不同的情境下会抛出不同的异常,这个时候可以尝试罗列多个 catch 语句,用于捕获不同类型的异常。
抛出异常
您可以使用 throw 语句在代码块中的任何地方抛出异常。throw 语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。
以下是尝试除以零时抛出异常的实例:
捕获异常
catch 块跟在 try 块后面,用于捕获异常。您可以指定想要捕捉的异常类型,这是由 catch 关键字后的括号内的异常声明决定的。
上面的代码会捕获一个类型为 ExceptionName 的异常。如果您想让 catch 块能够处理 try 块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号 ...,如下所示:
标准异常
C++中定义了一组类,用于报告标准库函数遇到的问题,也些异常可以在用户编写的程序中使用,在4个头文件中。
函数是一组一起执行一个任务的语句。每个 C++ 程序都至少有一个函数,即主函数 main() ,所有简单的程序都可以定义其他额外的函数。
您可以把代码划分到不同的函数中。如何划分代码到不同的函数中是由您来决定的,但在逻辑上,划分通常是根据每个函数执行一个特定的任务来进行的。
C++ 标准库提供了大量的程序可以调用的内置函数。例如,函数 strcat() 用来连接两个字符串,函数 memcpy() 用来复制内存到另一个位置。.
函数的形式如下
当您在一个源文件中定义函数且在另一个
创建 C++ 函数时,会定义函数做什么,然后通过调用函数来完成已定义的任务。
当程序调用函数时,程序控制权会转移给被调用的函数。被调用的函数执行已定义的任务,当函数的返回语句被执行时,或到达函数的结束括号时,会把程序控制权交还给主程序。
调用函数时,传递所需参数,如果函数返回一个值,则可以存储返回值。
形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。
当调用函数时,有两种向函数传递参数的方式:
默认情况下,C++ 使用传值调用来传递参数。一般来说,这意味着函数内的代码不能改变用于调用函数的参数。之前提到的实例,调用 max() 函数时,使用了相同的方法。
当您定义一个函数,您可以为参数列表中后边的每一个参数指定默认值。当调用函数时,如果实际参数的值留空,则使用这个默认值。
这是通过在函数定义中使用赋值运算符来为参数赋值的。调用函数时,如果未传递参数的值,则会使用默认值,如果指定了值,则会忽略默认值,使用传递的值。
C++11 提供了对匿名函数的支持,称为 Lambda 函数(也叫 Lambda 表达式)。
Lambda 表达式把函数看作对象。Lambda 表达式可以像对象一样使用,比如可以将它们赋给变量和作为参数传递,还可以像函数一样对其求值。
C++变量传递有传值和传引用的区别。可以通过前面的[]来指定:
对于[=]或[&]的形式,lambda 表达式可以直接使用 this 指针。但是,对于[]的形式,如果要使用 this 指针,必须显式传入:
分离式编译:分离式编译可以把程序分割到几个文件中去,每个文件独立编译
当形参引用类型时,我们所它的对应实参被引用传递(passed by reference)或者函数被传引用调用(called by reference)。和其他的引用一样,引用参数也是它绑定对象的别名,引用形参是它对应的实参的别名。当实参的数据拷贝给形参,形参和实参是两个相互的独立的对象,我们所这样的实参被值传递(passed by value)或者所函数被值调用(called by value)。
使用引用用于避免拷贝
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内),根本不支持拷贝的操作,当某种类型不支持拷贝的操作时,函数的引用只能通过引用形参是比较好。
使用引用形参返回额外的信息
一个函数只能返回一个值,然而有时候我们需要同时返回几个值,引用形参可以帮助我们实现返回多个值的结果。定义一个find_char函数,它返回string对象中某个字符第一次出现的位置。同时,我们希望返回字符出现的总次数。
指针或引用形参与const
在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。栈空间就是指放置程序的局部数据(也就是函数内数据)的内存空间。
内敛函数感觉类似宏替换,但是inline在替换前有类型检查,避免了宏的错误
在类体中定义的成员函数的规模一般都很小,而系统调用函数的过程所花费的时间开销相对是比较大的。调用一个函数的时间开销远远大于小规模函数体中全部语句的执行时间。为了减少时间开销,如果在类体中定义的成员函数中不包括循环等控制结构,C++系统会自动将它们作为内置(inline)函数来处理。
也就是说,在程序调用这些成员函数时,并不是真正地执行函数的调用过程(如保留返回地址等处理),而是把函数代码嵌入程序的调用点。这样可以大大减少调用成员函数的时间开销。C++要求对一般的内置函数要用关键字inline声明,但对类内定义的成员函数,可以省略inline,因为这些成员函数已被隐含地指定为内置函数。
应该注意的是,如果成员函数不在类体内定义,而在类体外定义,系统并不把它默认为内置(inline )函数,调用这些成员函数的过程和调用一般函数的过程是相同的。如果想将这些成员函数指定为内置函数,应当用inline作显式声明。
constexpr函数constexpr 函数是在使用需要它的代码时,可以在编译时计算其返回值的函数。当其参数为 constexpr 值并且在编译时使用代码需要返回值时(例如,初始化一个 constexpr 变量或提供一个非类型模板参数),它会生成编译时常量。使用非constexpr 参数调用时,或编译时不需要其值时,它将与正则函数一样,在运行时生成一个值。** constexpr和指针**如果关键字const出现在星号左边,表示被指物是常量(*const p);如果出现在星号右边,表示指针本身是常量(const *p);如果出现在星号两边,表示被指物和指针两者都是常量(*const *p)。与const不同,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关。
通常将内联函数和constexpr函数放在头文件中。
c/c++两种不兼容的函数指针形式:
(1) 指向C语言函数和C++静态成员函数的函数指针(2) 指向C++非静态成员函数的函数指针
不兼容的原因是因为在使用C++非静态成员函数的函数指针时,需要一个指向类的实例的this指针,而前一类不需要。
函数指针的定义方式
example
typedef 定义可以简化函数指针的定义**
** 函数指针同样是可以作为参数传递给函数的**
利用函数指针,我们可以构成函数指针数组,更明确点的说法是构成指向函数的指针数组。
指向类成员函数的函数指针
定义:类成员函数指针(member function pointer),是 C++ 语言的一类指针数据类型,用于存储一个指定类具有给定的形参列表与返回值类型的成员函数的访问信息。
基本上要注意的有两点:
1、函数指针赋值要使用 &2、使用 .* (实例对象)或者 ->*(实例对象指针)调用类s成员函数指针所指向的函数。
A) 类成员函数指针指向类中的非静态成员函数对于 nonstatic member function (非静态成员函数)取地址,获得该函数在内存中的实际地址。对于 virtual function(虚函数), 其地址在编译时期是未知的,所以对于 virtual member function(虚成员函数)取其地址,所能获得的只是一个索引值。
B) 类成员函数指针指向类中的静态成员函数
类成员函数指针与普通函数指针不是一码事。前者要用 .* 与 ->* 运算符来使用,而后者可以用 * 运算符(称为"解引用"dereference,或称"间址"indirection)。
普通函数指针实际上保存的是函数体的开始地址,因此也称"代码指针",以区别于 C/C++ 最常用的数据指针。而类成员函数指针就不仅仅是类成员函数的内存起始地址,还需要能解决因为 C++ 的多重继承、虚继承而带来的类实例地址的调整问题,所以类成员函数指针在调用的时候一定要传入类实例对象。C++1 typedef与decltype组合定义函数类型
decltype返回函数类型,add2是与add相同类型的函数,不同的是add2是类型,而非具体函数。
使用方法:
2 typedef与decltype组合定义函数指针类型
3 使用推断类型关键字auto定义函数类型和函数指针
4函数指针形参
说明:不论形参声明的是函数类型:void fuc2 (add2 add);还是函数指针类型void fuc2 (PF2 add);都可作为函数指针形参声明,在参数传入时,若传入函数名,则将其自动转换为函数指针。
5 返回指向函数的指针
1 使用auto关键字
2 使用decltype关键字
** 6 成员函数指针**
1普通成员函数指针使用举例
2继承中的函数指针使用举例
7重载函数的指针
1 重载函数fuc Void fuc(); Void fuc(int);2 重载函数的函数指针
类用于指定对象的形式,它包含了数据表示法和用于处理数据的方法。类中的数据和方法称为类的成员。函数在一个类中被称为类的成员。
也test类为例:
也下就进行说明:
类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。关键字 public、private、protected 称为访问修饰符。
public:公有成员在程序中类的外部是可访问的。您可以不使用任何成员函数来设置和获取公有变量的值.private:私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。默认情况下,类的所有成员都是私有的.protected:保护成员变量或函数与私有成员十分相似,但有一点不同,保护成员在派生类(即子类)中是可访问的。
继承中的特点有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。
但无论哪种继承方式,上面两点都没有改变:
private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;
protected 成员可以被派生类访问。
public继承
protected继承
privated继承
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。
函数的基本形式
example:
引入全局函数
对象以值传递的方式从函数返回
加入了一个静态成员,目的是进行计数。在主函数中,首先创建对象c1,输出此时的对象个数,然后使用c1复制出对象c2,再输出此时的对象个数,按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。拷贝构造函数没有处理静态static对象。如果要改变这一结果,我们就需要在拷贝构函数中加入count++语句,具体的实现如下所示:
浅拷贝将一个指针值赋值给另一个指针,就会使得两个指针指向同一块空间,这就产生了浅拷贝。
当两个(或两个以上)指针指向同一块空间,这个内存就会被释放多次;(例如下面定义了一个String对象s1,以浅拷贝的方式拷贝构造了一个String对象s2,则s1和s2里面的指针_str就会指向同一块空间;当出了作用域,s2先调用析构函数,而上面代码中析构函数里面进行了空间的释放,也就是这个空间就会被s2释放,接下来会调用s1的析构函数,也会去释放这块空间,释放一块已经不属于我的空间就会出错)其中深拷贝就可以解决这个问题。深拷贝**传统写法 **若用一个s1对象拷贝构造或赋值给s2对象,s2(s1)或 s2 = s1,当涉及到浅拷贝的问题时:对于拷贝构造函数来说,s2先开一块和s1一样大的空间;而对于赋值运算符重载函数来说s2已经存在,则必须先释放s2的空间然后才让s2开与s1一样大的空间(否则就会导致s2里面的指针没有释放)。然后让s2指向这块新开的空间,然后将s1里面的数据拷贝至s2指向的空间(自己开空间自己拷数据);
现代写法(调用其他的函数来实现自己的功能)
本质:让别人去开空间,去拷数据,而我将你的空间与我交换就可以。实现:例如用s1拷贝构造一个s2对象s2(s1),可以通过构造函数将s1里的指针_str构造一个临时对象tmp(构造函数不仅会开空间还会将数据拷贝至tmp),此时tmp就是我们想要的哪个对象,然后将新tmp的指针_ptr与自己的指针进行交换。对于构造函数来说,因为String有一个带参数的构造函数,则用现代写法写拷贝构造时可以调用构造函数,而对于没有无参的构造函数的类只能采用传统写法(开空间然后拷数据)。
引用计数的写时拷贝1.常用场景:
引用计数写时拷贝的改进1.为什么引用计数的写实拷贝需要改进?如果将引用计数单独的定义为一个int*的指针,它占4个字节,每次创建一个String对象,都会为其向操作系统申请4个字节的内存,这样就会经常申请许多小块内存,会造成内存碎片,也会对效率造成影响。2.这时可以考虑将_str与引用计数放在一起,就在_str的头上4个字节存放引用计数,当我们取引用计数时,只用将*((int*)(_str-4))
引用计数的写时拷贝,读有时也会拷贝例如对于String类,如果想要取出里面的某个字符或者修改某个对应位置上的字符需要重载operator[ ]:因为operator[ ]既可以读也可以修改,为了统一,无论读写,都需要重新拷贝;
类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。破坏了类的封装。
友元函数的定义如下:
友元类同友元函数一样,一个类可以将另一个类声明为友元类。若A类为B类的友元函类,则A类中的所有成员函数都是B类的友元函数,都可以访问B类的私有和保护成员。
注意友元关系不能传递,友元关系是单向的,友元关系不能被继承。
C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。
如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略 inline 限定符。
在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。
每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。this指针记录每个对象的内存地址,然后通过运算符->访问该对象的成员.友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。
静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化。
一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 ->,就像访问指向结构的指针一样。与所有的指针一样,您必须在使用指针之前,对指针进行初始化。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名。
单继承
菱形继承
**承与基类成员在派生类中的访问关系表 **
**继承与转换 **1)子类对象可以赋值给父类对象。2)父类对象不可以赋值给子类对象。3)父类对象的指针和引用可以指向子类对象。4)子类对象的指针和引用不能指向父类对象,但是可以通过强制转化完成。
运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造,一般来讲,重载的功能应当与原有功能相类似,不能改变原运算符的操作对象个数,同时至少要有一个操作对象是自定义类型。可以进行运算符重载的有:
不能重载的运算符只有5个,它们是类属关系运算符“.”、成员指针运算符“.*”、作用域分辨符“::”、sizeof运算符和三木运算符“?:”
友元函数的类型
内部函数的形式
有时候,我们得提供一些接口给别人使用。接口的作用,就是提供一个与其他系统交互的方法。其他系统无需了解你内部细节,并且也无法了解内部细节,只能通过你提供给外部的接口来与你进行通信。根据c++的特点,我们可以采用纯虚函数的方式来实现。这样做的好处是能够实现封装和多态。
也下是interfacedefineandrealize.h头文件的定义:
新建项目,加载上述三个文件,设置项目属性—>配置属性——>常规——>配置类型 ,选择"动态库.dlll",生成可用的动态库,假如项目名称为InterfaceDefineAndRealize(注意:项目名称必须与模块定义文件中 LIBRARY 后面定义的名字相同,否则将导致出现无法找到动态库的错误。),则在该项目的当前工作目录下位生成动态库和它的导入库。
接口的调用
方法1:项目属性——>配置属性——>C/C++——>常规——>附加包含目录 将include文件夹的全路径添加进来。
方法2:项目属性——>配置属性——>VC++目录——>包含目录 中将include文件夹的全路径添加进来。
方法1:项目属性——>配置属性——>链接器——>常规——>附加库目录 将lib文件夹的全路径添加进来。
方法2:项目属性——>配置属性——>VC++目录——>库目录 将lib文件夹的全路径添加进来。
注意:2.1中的方法1与2.2中的方法1对应,2.1中的方法2与2.2中的方法2对应,不能不换使用。
将生成的.dll动态库放到项目的当前目录下的Debug目录下,防止出现缺少动态库的错误。
-------------------------------------------