标准输出流中 cout
是一个ostream对象,<<
和 >>
是C++中经过重载
的运算符,配合cout和cin使用时表示流运算符。C++中是如何重载运算符的?
cin 从键盘读取输入的时候,首先会忽略掉开头的任意多个空格输入,只会从第一个非空格符、制表符和回车符
开始读取。开始读取后,cin会以一个回车符作为输入的结束标志,但不会读取这个回车符!因此这个回车符还会保存在输入缓存中。
运算符优先级:比较运算符 大于 赋值运算符
为什么需要.h头文件?因为头文件中包含了一些全局属性的声明。在C++中,函数可以直接定义在类之外,因此有函数和方法的概念。函数是哪些直接定义在类外的,而方法是定义在类内部的。函数和方法的结构和功能都一致,仅仅是位置不同。定义在类之外的函数,是全局可用的,这点和类一致。因此,需要在使用某个函数的时候进行声明。当函数的声明变多的时候,头文件就起到了包含这些所需要声明方法的作用。一般的,某个源文件中通过同名的头文件来进行声明,需要使用该源文件中功能的时候,包含其头文件即可。
内部链接
,即该变量或函数仅仅在文件内部是可见的,外部无法进行访问。在声明变量时,变量具有静态持续时间,即生命周期从程序启动时新生到到程序结束时死亡。并且除非您指定另一个值,否则编译器会将变量初始化为0。与Java一致
)与Java一致
)在基本数据类型中,除了void之外,其他数据类型都可以进行算数运算。
在进行算数运算中,精度不同的数据类型进行运算时,运算结果的数据类型取精度大的那个数据类型。
取余运算符%的两个操作数必须是整数类型
赋值运算的规则:
短路求值:||的左侧为真,则会跳过右侧判断; &&左侧的结果为假,则会跳过右侧判断。可以用于特定左侧条件下执行右侧语句,能否代替if条件语句?
在C++中,如果要进行位移运算,那么长度小于int的数据类型会默认提升到int类型,运算的结果也是int类型。
在左移操作时,默认左移的位数是小于32的,如果大于等于32,则左移的位数会对32取模。例如左移34位相当于左移2位。
在自动类型转换的时候,一般会将长度较小的类型转换到长度较大的类型,从而避免精度丢失。但是在将浮点型转换为整数类型是,会丢弃小数部分,只保留整数部分,造成精度丢失。
C++中强制类型转换的语法有三种:
int total = 20, num = 6;
// 方式一:c语言风格
double avg = (double)total / num;
// 方式二:c++风格
double avg = double(total) / num;
// 方式三:c++强制类型转换运算符
double avg = static_cast(total) / num;
switch-case中,每个case一般会配合break使用,表示跳出当前switch。如果case中没有加break,则会继续执行其他case中的语句,即使case没有匹配,直到遇见break。
switch(变量){
case 值1:
...;
break;
case 值2:
...;
break;
...
default:
...;
}
C++中的for循环和Java一样,都有两种方式:普通for循环和增强for循环。
C++中有goto语句,只需要在某个代码上面做个标记,就可以使用goto进行跳转。goto语句非常灵活,但是也非常危险,很容易造成死循环。
数组的定义
int a1[10];
const int n = 4;
double a2[n];
int a3[4] = {1, 2, 3, 4};
double a4[] = {1.1, 2.2, 3.3}; // 自动推断长度
short a5[10] = {3, 6, 9};
short a6[2] = {1, 2, 3}; // 报错,初始值太多
int a6[4] = a3; // 报错,不能用另一个数组对数组进行赋值
在定义数组时,数组的长度必须为常量,如果是变量会报错。(Java不会)
在方法中定义的数组的长度必须大于0
int a[0]; //会报错,数组元素必须大于0
空数组可以定义,但仅限于类或者结构体中
在Visual studio中,对为定义的局部变量赋值为0xcc。如果直接打印,则会打印出"烫烫烫烫…"。
C++中数组的长度是如何获取的?
数组所占空间 = 数据类型所占空间大小 * 元素个数
元素个数 = 数组所占空间 / 数据类型所占空间大小
C++中,如果对数组进行越界访问,并不会报错,而是接着数组的最后一个元素的内存位置继续向后访问。因此可能访问到其他程序正在占用的内存。所以在C++中使用数组,要严格限制访问的下标在数组范围之内。
多维数组初始化
int ia[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int ia2[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
int ia3[][4] = {1, 2, 3, 4, 5, 6, 7}; // 自动推断
vector详解(相当于Java中的List)
初始化方式:
// 默认初始化
vector v1;
// 拷贝初始化
vector v2 = {'a', 'b', 'c'};
// 等号可以省略
vector v3{'a', 'b', 'c'};
// 直接初始化,定义一个初始长度为5的容器,默认初始化值为0
vector v4(5);
// 直接初始化,定义一个长度为5的容器,默认初始化值为100
vector v5(5, 100);
访问元素:可以通过下标访问。越界访问会报错退出。
添加元素:
v5.push_bask(69);
除了vector之外,C++ 11还提供了一个array模板类,它跟数组更加类似,长度是固定的,但更加方便,更加安全。所以在实际应用中,一般推荐对于固定长度的数组使用array
,不固定长度的数组使用vector
。
字符串详解
初始化方式
// 默认初始化
string s1;
// 拷贝初始化
string s2 = s1;
string s3 = "Hello world";
// 直接初始化
string s4("Hello world");
string s5(5, 'a');
访问字符:可以通过下标进行访问
cout << s4[2] << endl; // 输出l
s4[0] = 'h'; // 字符串变为 hello world
cout << s4[s4.size() - 1] << endl;
字符串拼接
string str1("hello"), str2 = "world";
string str3 = str1 + str2;
+运算符重载的条件就是必须基于一个string对象
。在多个string对象和多个字符串常量同时做相加时,需要满足左结合律,即+左边必须是一个string对象。字符串比较
string str2 = str1;
这样的语句,str1 与 str2都是不同的对象,在内存中的地址也是不一样的。字符数组(c语言风格字符串)
在c语言中,并没有字符串类型。字符串都是以char[]的形式保存的。
在c语言中,可以通过如下形式定义字符串
char str1[] = {'a', 'b', 'c', 'd', 'e', 'f', '\n'};
char str2[] = "abcdef";
一个字符串的结束标识是\0
,在进行打印的时候,printf和cout总是会在遇到\0时结束打印。使用char str2[] = "abcdef";
方式定义字符串是,会默认在末尾增加一个隐藏的\0。
C++为了兼容c语言,字符串常量都是以char[]形式进行存储的。
一般推荐直接使用string,尽量少使用字符数组来表示字符串。
命令行输入输出
文件输入输出
结构体的 定义 必须在 使用 之前。
枚举:
枚举类型内部只有有限个名字,他们各自代表了一个常量
,被称为枚举量
。
默认情况下,会将整数值赋值给枚举量,默认从0开始,每个枚举量依次加1
enum week{
// 分别对应着 0 ~ 6的常量
Mon, Tue, Wed, Thu, Fri, Sat, Sun
}
可以通过对枚举量赋值,显式的设置每个枚举量的值。
如果直接用一个整型值对枚举类型赋值,将会报错,因为类型不匹配;
可以通过强制类型转换,讲一个整型值赋给枚举对象;
最初的枚举类型只有列出的值时有效的;而C++通过强制类型转换,允许扩大枚举类型合法值的范围。不过一般使用枚举类要避免直接强转赋值。
指针
概念:指针顾名思义,就是"指向"另外一种数据类型的复合类型。指针是C/C++中一种特殊的类型,他所保存的信息,其实是另外一个数据对象在内存中的’'地址"。通过指针可以访问到指向的那个数据对象,所以这是一种间接访问对象的方法
。指针类型的长度是固定的,与操作系统寻址空间有关。
所谓64位系统和32位系统,说的是内存寻址空间的长度是64位bit和32位bit。在64位系统中,内存寻址空间从0 ~ (2^64 - 1)。因此指针中可以保存的最大的地址是2^64 - 1,共需要8个字节来存储地址信息。而在32位系统中,内存寻址空间从0 ~ (2^32 - 1),因此只需要4个字节来存储地址信息。
指针指向的地址,指的是指向对象的首地址。因为数据类型的长度是不同的,因此知道是何种类型的指针和首地址后,解引用操作就可以通过首地址加偏移量获取指向对象的值。
指针类型定义如下:
int a = 10;
int* p1 = &a;
long l = 20;
long* p2 = &l;
// 不可以将p2指针指向 &a,因为类型不匹配
// 指针类型指的就是int* 、 long* 这些类型,而p1 、p2 是指针类型的变量
解引用
// 可以使用* 对指针类型的变量进行解引用
cout << p1 << endl; // 输出00000059362FF7B4,是变量a的地址
cout << *p1 << endl; // 输出10
*p1 = 20;
cout << *p1 << endl; // 输出20
一些特殊指针
无效指针
:没有初始化的指针,这些指针指向的地址是不确定的,因此使用这些指针是非常危险的!一般来说编译器是不会让无效指针通过编译的。无效指针也叫做野指针
,不能使用野指针。
空指针
:先定义了一个指针,但还不知道它要指向哪个对象,这是可以把它初始化为"空指针"。有三种方法定义空指针:
// 三种方式本质上都一样
int* np = nullptr;
np = NULL;
np = 0;
void* 指针
:表示该类型的指针可以指向任意类型的数据对象。但无法解引用,因为不知道指向的数据类型的长度。一般void* 类型的指针只用来存放指针的地址,不做解引用操作。
二级指针:指向指针的指针,即保存的是一级指针的地址。对二级指针进行解引用将会得到一级指针的值,一级指针的值本质上也是一个地址,需要在用一次解引用后获得所指向的基本数据类型的值。也就是说,解引用符*
操作地址
可以获得对应地址
的值。
指向常量的指针和指针常量
int a = 100;
int b = 200
const int num1 = 10;
const int num2 = 20;
// 指向常量的指针
const int* pc = &num1;
cout << *pc << endl; // 输出10
pc = &num2;
cout << *pc << endl; // 输出20
// *pc = 25; // 错误,无法改变常量
// 指针常量
int* const cp = &a;
cout << *cp << endl; // 输出100
*cp = 150;
cout << *cp << endl; // 输出150
// cp = &b; // 错误,无法修改常量
// 指向常量的指针常量
const int* const cpp = &num1;
// cpp = &num2; // 错误,无法修改常量
// *cpp = 15; // 错误,无法修改常量
指针和数组
指针数组和数组指针
编译器会根据类型和地址来确定解引用的结果
。int arr[5] = {1, 2, 3, 4, 5};
// 指针数组
int* pa[5] = {nullptr, nullptr, nullptr, nullptr, nullptr};
// 数组指针
int (*ap) [5];
ap = &arr; // arr是指向arr数组的第一个元素的指针,arr的值是数组中第一个元素的首地址
cout << ap << endl;
cout << arr << endl;
关于数组名和指针的一些思考关于C++数组名和指针的一些思考
引用:定义变量的别名,类似于快捷方式。
引用的定义
int a = 10;
int b = 25;
// 一旦定义引用的对象,之后就不可以更改
int& ref = a;
// int& ref; 报错!引用必须被初始化。
// int& ref = 10; 报错!变量的引用必须指向对象,而不是字面值。
// ref = b;并不代表引用指向b,而是ref的值被修改为25,即a被修改为25。
ref = b;
引用时内存中变量的别名,本身并不占据内存空间,一旦定义引用,就与指向的变量是共同的内存地址。
引用的引用:套娃,就是别名的别名,本身还是指向同一个对象。
常量引用:顾名思义,就是对常量的引用。但是在初始化时,有时可以不必指向常量。对常量引用的初始化要求非常宽松。
const int ci = 20;
int i = 30;
const int& cref = ci;
const int& cref2 = i; // 正确,常量引用可以直接引用变量
const int& cref3 = 10; // 正确,常量引用可以直接引用字面量
double d = 3.14;
const int& cref4 = d; // 正确,此时d被隐式转化为int类型的值 3,cref4引用了字面量3
// cref2 = 10; 错误!虽然cref2引用变量,但常量引用不能修改值
引用可以看做是指针常量解引用的语法糖,但本质上并不是指针常量的解引用,因为指针常量需要占据内存,引用不占据内存。
可以有对指针的引用
int a = 10;
int* ptr = &a;
int*& pref = ptr;
// 操作pref就是操作ptr
cout << *pref << endl; // 输出10;
没有指向引用的指针,因为指针保存的是内存地址,而引用只是别名,并不进行存储,因此没有地址。
C++中的箭头运算符 ->
:是解引用可访问成员两个操作的结合;这样就可以很方便的表示"取指针所指向内容的成员"
Student s1 = Student("阿秋", 25);
Student* p = &s1;
// 下面两个语句是等价的
cout << (*p).getName() << endl;
cout << p -> getName() << endl;
C++中的对象赋值:就是把对象的内容复制过去,对象的地址不发生改变。浅谈C++和Java中对象的等号赋值
全局变量和局部变量:每个变量都有其作用域,一个作用域就是一对{}
,在{}内定义的变量,作用域只在该{}内。定义在{}之内的变量由于就称为局部变量
。而定义在所有{}之外的变量,它的作用域是全局可见的,被称为全局变量
。
自动对象和静态对象
自动对象
。方法中的形参也是一种自动对象。对于自动对象来说,它的生命周期和作用域是一致的。局部静态对象
。注意,局部静态对象依然只有局部的作用域,在作用域之外依然是不可见的。但它的生命周期贯穿了整个程序运行过程,只有在程序结束时才被销毁,这一点与全局变量类似。静态对象如果不在代码中做初始化,基本数据类型会被默认初始化为0值。函数的声明
先声明再使用
,而不是先定义在使用,因此可以先声明后定义
。参数传递:参数传递的本质,其实就是使用实参对形参进行初始化赋值。
传值:将实参的值赋值给形参,实参和形参在内存中是两份实体,并不相互影响。
传引用:将形参定义为引用,实参也是引用。将引用传递给引用,其实就是引用的引用,所指向的对象是相同的。
传引用的好处是:可以对函数外部数据对象进行更改
;可以避免数据复制
trick:在定义引用类型的参数的时候,如果不会更改引用对象本身的内容,那么可以把参数类型定义为常亮引用。这样做的好处是能够扩大传参的范围。因为引用变量是无法用字面值初始化的,常亮引用是可以用字面值初始化的。
void fun(const string& str1, const string& str2) {
cout << str1 << endl;
cout << str2 << endl;
}
void fun2(string& str1, string& str2) {
cout << str1 << endl;
cout << str2 << endl;
}
int main() {
// 正确
fun("hello", "world");
// 报错,无法用常亮初始化引用变量
fun2("hello", "world");
return 0;
}
传数组:有三种方式传数组,前两种会把传入的数组退化成一个指针。一般建议使用传数组引用的方式。
void fun1(int* arr) {
cout << (sizeof arr) << endl;
}
void fun2(int arr[5]) {
cout << (sizeof arr) << endl;
}
void fun3(int (& arr) [5]) {
cout << (sizeof arr) << endl;
}
int main() {
int arr[] = { 1, 2, 3, 4, 5 };
cout << sizeof arr << endl;
fun1(arr); // 输出8
fun1(arr); // 输出8
fun1(arr); // 输出20
return 0;
}
返回类型
auto
关键字,在函数尾部使用 -> 数据类型
的方式来声明函数。此时函数的返回类型就是尾部声明的类型。内联函数:函数声明前 使用 inline关键字,表示该函数在调用时,直接拷贝函数体内的代码到调用处,而不做函数调用。省去了保存上下文和函数入栈等操作。在某种程度上可以优化运行效率。但最近研究表明,inline函数的功能已经发生了改变,编译器会自动进行内联优化,如果仅仅是为了优化效率,则不建议使用inline。现代C++里,inline被理解成:它通知编译器,这个符号可能会在多个翻译单元中重复出现。大坑inline函数
函数参数默认值
在C++中,可以定义函数参数的默认值,直接在函数定义的时候在参数列表后面使用等号为参数设置默认值即可。在调用函数进行传参时,python是可以显式的指定参数名从而为指定形参赋值的。与python不同,C++无法跳过某些默认值进行之后的参数设置,必须严格按照参数顺序进行传参。
在Java中无法设置函数参数默认值,但是可以通过函数重载来达到相同的目的。
函数重载
和Java一样,C++支持函数重载。而C语言和python并不支持函数重载。在python中,后定义的函数会覆盖先定义的函数。
const类型的函数重载
函数匹配
void fun(int x){
cout << 1 << endl;
}
void fun(int x, int y){
cout << 2 << endl;
}
void fun(double x, double y = 1.5){
cout << 3 << endl;
}
int main(){
// 输出3
fun(3.14);
// 发生二义性调用。报错!有多个重载函数fun实例与参数列表匹配
fun(3.14, 10);
}
二义性调用
。在Java中,函数参数匹配时,只可以发生向下的隐式类型转换,例如int类型传递给double类型。无法在丢失精度的情况下发生类型转换。并且int型优先匹配为long,而不是double。
函数重载必须在同一作用域中。如果在不同作用域中声明
同名函数,内层的函数会覆盖外层的声明或定义的其他同名不同参数的函数
。
void fun(int i){
cout << i << endl;
}
void fun(double i){
cout << i << endl;
}
void fun(string i){
cout << i << endl;
}
int main(){
void fun(int i);
// 可行,调用fun(int i)
fun(10);
// 可行,调用fun(int i),发生参数类型隐式转换
fun(3.14);
// 报错!无法调用fun(string i)
fun("hello");
}
函数指针和函数引用
函数其实也是有类型的,函数的类型由其返回值和参数类型共同决定,与函数名和参数名无关。
void testDefault(double i) {
cout << 1 << endl;
}
void testDefault(long i) {
cout << 2 << endl;
}
int main() {
// 定义函数指针
void (*fp) (long) = nullptr;
// 以下两种赋值语句等价
fp = testDefault;
fp = &testDefault;
// 以下两种调用方式等价,输出3
(*fp)(3);
fp(3);
// 定义函数引用
void (&fr) (long) = testDefault;
// 也可以调用函数,输出3
fr(3);
}
函数指针和函数引用作为函数的参数。感觉比较离谱
作用基本相同,唯一不同的是函数指针的引用作为参数时,无法使用函数名作为参数传入,此时需要定义为const
python中也有函数作为参数的概念,在Java中可以使用接口和匿名内部类的方式来实现。不过C++中的语法还是太离谱了,很多种方式都可以。
typedef decltype(funName) newName:提取funName函数的类型,定义函数类型为新的名称newName
疯了!下面的代码除了注释的部分,其他都没问题,请自行理解
。
void testDefault(long i) {
cout << i << endl;
}
// 以下几种定义方式相同,可以省略指针符号*
void fun1(long i, void (*fp) (long)) {
// 以下两种调用方式相同,可以省略解引用符号*
(*fp)(i);
fp(i);
}
void fun2(long i, void fp(long)) {
// 以下两种调用方式相同,可以省略解引用符号*
(*fp)(i);
fp(i);
}
// 使用类型别名,以下方式相同,函数名本身就被编译器解析为指针
typedef void (*Funcp)(long);
typedef void Func(long);
typedef decltype(testDefault) Func;
void fun3(long i, Funcp fp) {
// 以下两种调用方式相同,可以省略解引用符号*
(*fp)(i);
fp(i);
}
void fun4(long i, Funcp& fp) {
// 以下两种调用方式相同,可以省略解引用符号*
(*fp)(i);
fp(i);
}
void fun5(long i, const Funcp& fp) {
// 以下两种调用方式相同,可以省略解引用符号*
(*fp)(i);
fp(i);
}
void fun6(long i, Func fp) {
// 以下两种调用方式相同,可以省略解引用符号*
(*fp)(i);
fp(i);
}
void fun7(long i, Func& fp) {
// 以下两种调用方式相同,可以省略解引用符号*
(*fp)(i);
fp(i);
}
void fun8(long i, Func* fp) {
// 以下两种调用方式相同,可以省略解引用符号*
(*fp)(i);
fp(i);
}
Func& fun9() {
return testDefault;
// return &testDefault; 报错
}
Func* fun10() {
return testDefault;
return &testDefault;
}
Funcp fun11() {
return testDefault;
return &testDefault;
}
Funcp& fun12() {
Funcp f = testDefault;
return f;
}
int main() {
// 以下两种调用方式相同,可以省略去地址符号&
fun1(10, testDefault);
fun1(10, &testDefault);
fun2(10, testDefault);
fun2(10, &testDefault);
fun3(10, testDefault);
fun3(10, &testDefault);
Funcp f = testDefault;
Funcp& ref = f;
fun4(10, f);
fun4(10, ref);
//fun4(10, testDefault); 报错,无法使用字面量初始化一个引用
//fun4(10, &testDefault); 报错,无法使用字面量初始化一个引用
fun5(10, &testDefault);
fun5(10, testDefault);
fun6(10, &testDefault);
fun6(10, testDefault);
fun7(10, testDefault);
fun8(10, &testDefault);
fun8(10, testDefault);
fun1(10, fun9());
fun1(10, fun10());
fun1(10, fun11());
fun1(10, fun12());
fun2(10, fun9());
fun3(10, fun10());
// fun4(10, fun11()); 报错,字面量无法赋值引用变量
fun5(10, fun12());
fun4(10, fun12());
}