从C开始的CPP学习生活01-基础内容

0、写在前面

本笔记为学习cpp过程中记录的一些知识,并不全面,也并不一定完全正确

参考视频教程:bilibili

代码测试环境:win10 + vs2017

1、换行

endl换行并清空并刷新缓冲区

\n只进行换行

2、newdelete和mallocfree的区别

new和delete会触发构造函数和析构函数

malloc和free不会

3、引用

3.1、引用是给我们的变量起别名

1
2
3
4
5
int a = 12;
int &c = a;//声明变量a的一个引用c

c = 13;
std::cout << a << std::endl;

打印13

注意:引用声明的时候一定要初始化int& d;是非法的

​ 一个变量可以有无数个引用

3.2、常量的引用

1
const int& a = 12;

a不可修改,const是必须的

3.3、数组的引用

1
2
3
4
5
int arr[12];
int(&x)[12] = arr;//p的用法和arr相同

int arr2[2][3];
int(&p)[2][3] = arr2;

3.4、指针的引用

1
2
3
int b = 1;
int *point = &b;
int* (&q) = point;

3.5、引用做参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Exchange(int& a, int& b) 
{
int nTemp = a;
a = b;
b = nTemp;
}

int main()
{
int a = 13, b = 15;
std::cout << "a:" << a << std::endl;
std::cout << "b:" << b << std::endl;
Exchange(a, b);
std::cout << "a:" << a << std::endl;
std::cout << "b:" << b << std::endl;

return 0;
}

3.6、引用做返回值

不要引用局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
int& fun()
{
int a = 12;
return a;
}

int main()
{
int& b = fun();
std::cout << b << std::endl;

return 0;
}

warning:warning C4172: returning address of local variable or temporary: a

操作非法内存的结果是未知的,所以不要这样使用

局部变量所占用的内存空间的分配和销毁取决于编译器的实现,编译器为了优化程序性能,可能有不同的策略来分配、释放内存

比如:VC编译器可能在函数入口处即分配这里的全部变量,GCC编译器也可能在定义出才分配变量

因此除了之哟用于的局部变量是不允许访问的,该空间可能暂时没有被释放,也可能被释放,所以不要访问这样的内存。

3.7、引用与指针的区别

  • 引用声明就要初始化,指针不用
  • 引用初始化之后就不能指向其他空间了,指针可以
  • 引用不占存储空间,指针占空间
  • 引用效率更高,指针属于间接操作
  • 引用更安全,指针可以偏移
  • 指针更灵活,直接操作地址,指针更通用

4、参数缺省

4.1、函数定义

全部指定:

1
2
void fun(int a = 12, char c = 'v') //全部指定
{}

部分指定,部分指定必须从右向左连续指定,不能指定左侧的参数而右侧参数未指定

1
2
void fun1(int a, char c, float f = 123.123f) 
{}

不满足这条规则会报错:
Error (active) E0306 default argument not at end of parameter list
Error C2548 'fun1': missing default parameter for parameter

4.2、函数调用

函数调用时,有默认值的参数,可以不用传递实参

没有指定默认值的,一定要传递实参

有默认值还传递实参,实参会覆盖掉默认值

5、函数重载

5.1、定义

同一个作用域内,函数名字相同,参数列表不同(参数类型不同或者参数个数不同)

1
2
3
4
5
6
7
8
9
10
11
12
void fun(int a)
{
std::cout << "fun1" << std::endl;
}

void fun(int a, int b) {
std::cout << "fun2" << std::endl;
}

void fun(char a) {
std::cout << "fun3" << std::endl;
}

根据参数不同可以调用不同的函数

5.2、二义性问题

但是,考虑到缺省值的问题,不合理的使用缺省值会导致重载函数产生二义性,比如:

1
2
3
4
5
6
7
8
void fun(int a)
{
std::cout << "fun1" << std::endl;
}

void fun(int a, int b = 12) {
std::cout << "fun2" << std::endl;
}

这里面函数互为重载函数,但是再调用fun(12)是编译器会报错:
Error C2668 'fun': ambiguous call to overloaded function

所以在使用函数重载时要谨慎使用缺省值

5.3、返回值问题

函数重载不包括返回值,因为C++是可以不考虑返回值的,void类型的数据也可以return 0;bool类型的数据也可以return 1;

6、防止头文件重复包含

两种方法

1
2
3
#ifndef AAA
#define AAA
#endif

另一种方法

1
#pragma once

第二种方法能否使用取决于编译器,因此使用这种方法移植性特别差,所以不建议使用

7、类

类是具有相同属性和行为的对象的集合

7.1、类的声明

1
2
3
4
5
6
7
8
9
10
#include <iostream>

class CPeople {
public:
int a;
void fun()
{
std::cout << "fun" << " " << a << std::endl;
}
};

7.3、声明对象及成员调用

1
2
3
4
5
6
7
8
9
10
11
int main()
{
CPeople op;
CPeople* op1 = new CPeople;
op1->a = 9;
op1->fun();
op.a = 12;
op.fun();

return 0;
}

类的所有成员(个别特殊的static),必须通过对象访问。

7.4、访问修饰符

作用范围:书写位置开始,一直到下一个修饰符,或者类结尾的花括号

7.4.1、public

public修饰符对外可见,其他的类也可以访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CPeople {
public:
int a;
void fun()
{
std::cout << a << " " << "fun" << std::endl;
}
};

class CBird {
public:
CPeople p;
void fun2() {
p.fun();
}
};

C++结构体是特殊的类,访问修饰符默认是public

7.4.2、private

类里面不注明修饰符默认是private修饰符

private是私有修饰符,其成员只有类内可以访问

1
2
3
4
5
6
7
8
class CPeople {
int a;
public:
void fun()
{
std::cout << a << " " << "fun" << std::endl;
}
};

a是私有成员,fun方法中可以调用

7.4.3、protected

protected修饰符修饰的成员对类内以及子类可见

8、友元

友元 类内成员对于这个函数/类来说相当于public,protected修饰符也可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class CStu
{
private:
int age;
void fun()
{
age = 12;
std::cout << age << std::endl;
}

friend void fun1();
friend class CTeach;
};

class CTeach {
public:
CStu stu2;
void fun2()
{
stu2.fun();
}
};

void fun1()
{
CStu cstu;
cstu.fun();
}

特点:

  • 不受访问修饰符的影响
  • 破坏了类的封装性

9、函数成员

9.1、构造函数

9.1.1、普通构造函数

类中的成员初始化有三种方法:

  • 定义时直接赋值(比较古老的C++版本不支持)
  • 初始化列表
  • 构造函数里面初始化
1
2
3
4
5
6
7
8
9
class CStu
{
public:
int age = 13;
CStu() : age(2)
{
age = 3;
}
};

三种初始化方式优先级依次递增,所以age打印出来的时候是3

构造函数在对象创造过程中调用的,只有栈区对象会调用构造函数,栈区对象就是普通的变量,堆区的对象不会调用构造函数,比如指针对象

1
CStu* stu;

此时不会调用构造函数

9.1.2、带参数的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CStu
{
public:
int age;
float f;
CStu(int a, float b) : age(a), f(b) {

}
};


int main()
{
CStu stu1(2, 2.3f);
std::cout << stu1.age << " " << stu1.f << std::endl;
}

参数并不需要完全用于初始化

构造函数也可以指定默认值

1
2
3
4
5
6
7
8
9
class CStu
{
public:
int age;
float f;
CStu(int a, float b = 12.23f) : age(a), f(b) {

}
};

但是会被实际的传参值覆盖掉,优先级问题。

指针对象初始化

1
CStu stu2 = new CStu(2, 45.56f);

9.1.3、多个构造函数构成重载

根据参数传递区分使用哪个构造函数

9.1.4、类中函数的声明与定义(实现位置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CStu
{
public:
int age;
float f;
CStu(int a, float b);//不可以给b缺省值

void fun();
};

CStu::CStu(int a, float b = 12.35f) {
age = a;
f = b;
}

void CStu::fun()
{
std::cout << "fun" << std::endl;
}

可以在类内实现,也可以在类外实现

但是,在定义函数时不可以添加缺省值

9.2、构造函数初始化列表

9.2.1、初始化与赋值的区别

初始化时一个变量或者对象产生时就赋予一个初始值,伴随性质

赋值是一个变量或者对象产生之后的任意时刻可以赋予一个值,随意性质

宏观代码上的区别

  • 基本数据类型:作用完全相同;
  • 数组、结构体:数组和结构体初始化时可以采用花括号的形式赋值,初始化之后的赋值不可以使用花括号赋值;作用也完全相同
  • 引用、const:一定要进行初始化,不能赋值;const变量不初始化报错:'const' object must be initialized if not 'extern'

9.2.2、构造函数与初始化列表的区别

构造函数的本质是赋值

1
2
3
4
5
6
7
8
9
10
11
class CStu
{
public:
int a;
float f;
CStu()
{
a = 13;
f = 13.23f;
}
};

初始化列表的本质是初始化

1
2
3
4
5
6
7
class CStu
{
public:
int a;
float f;
CStu() : a(13), f(13.23f){}
};

9.2.3、通过参数进行构造函数初始化

1
2
3
4
5
6
7
class CStu
{
public:
int a;
float f;
CStu(int b, float c) : a(b), f(c){}
};

在CStu初始化时需要使用

1
CStu stu(12, 45.6f);

我们不需要通过参数初始化所有的列表

1
2
3
4
5
6
7
class CStu
{
public:
int a;
float f;
CStu(int b) : a(b), f(15.56f){}
};

或者有不被使用的参数也可以

9.2.4、类内变量的初始化顺序

变量的初始化顺序只与声明顺序有关,与初始化列表中的顺序无关

在前面的例子中,我们对CStu初始化时首先初始化a,然后初始化f

改变写法

1
2
3
4
5
6
7
class CStu
{
public:
int a;
float f;
CStu(int b) : f(15.56f), a(b){}
};

同样先对a初始化,然后再初始化f

9.2.5、在成员变量之间进行初始化

1
2
3
4
5
6
7
class CStu
{
public:
int a;
int f;
CStu(int b) : f(a), a(12){}
};

同样,根据9.2.4中的描述我们可知,我们会先对a进行初始化,然后对f初始化,所以两个变量都是12

1
2
3
4
5
6
7
class CStu
{
public:
int a;
int f;
CStu(int b) : f(12), a(f){}
};

上面这种初始化方式则不正确,我们将a初始化为f,这是一段未赋值的数据,会出现随机数,也可能根据编译器的不同造成报错。

9.3、引用和const的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CStu {
public:
int f;
int &a;
const int e;
CStu(int c) : a(c), f(13), e(18)
{

}


void show() {
std::cout << a << ' ' << f << std::endl;
}
};

注意:初始化之后相当于a和f是一个变量

同时,对引用进行初始化的时候需要注意生命周期的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CStu {
public:
int f;
int &a;
CStu(int c) : a(c), f(13)
{

}

void show() {
std::cout << a << ' ' << f << std::endl;
}
};

int main()
{
CStu stu1(12);
stu1.show();
return 0;
}

此时打印的a会是一个无效的数字,因为此时12这个局部变量的生命周期已经结束,引用不再有效。在构造函数内部打印则是12。

9.4、析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CStu
{
public:
int nAge;
CStu()
{
nAge = 12;
}
~CStu()
{

}
};

  • 析构函数中没有参数(没有重载)
  • 析构函数前面需要一个~
  • 默认的析构函数什么都不做
  • 对象生命周期结束时,自动调用
  • 指针对象存放在堆中,声明时需要使用new才能触发构造函数,在使用delete或者整个程序结束时触发析构函数
  • 临时对象
1
2
3
4
5
6
int main()
{
CStu();

return 0;
}

CStu();就是一个临时对象,临时对象的作用域就是当前所在的语句。

9.5、mallocfree和new delete的区别

malloc free不会触发构造函数和析构函数

new delete会触发构造函数和析构函数

9.6、this

看下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class CStu
{
public:
int a;
CStu(int a) {
a = a;
}

void show()
{
std::cout << a << std::endl;
}
};


int main()
{
CStu stu1(5);
stu1.show();
return 0;
}

这段代码在执行之后,打印a的值为-858993460

原因是在构造函数的作用域中,构造函数只认得a是通过参数传递过来的a,而类本身的成员(属性)a构造函数并不承认,所以最后在打印a的值的时候,会是一个随机值。

为了解决这样的问题,C++给出了this指针

1
2
3
4
5
6
7
8
9
10
11
12
13
class CStu
{
public:
int a;
CStu(int a) {
this->a = a;
}

void show()
{
std::cout << a << std::endl;
}
};

this指针是指向当前对象的指针。

  • this指针是在对象创建时才存在的
  • this指针的类型是其所指向的对象的类型
  • this指针不是成员,不能使用stu.this等方法
  • this指针是类成员函数的隐含参数,每个函数都隐含一个this参数,所以我们只能在函数里面使用this指针,函数外不可使用
  • this指针的作用域在类的内部,准确来说在类的函数内部

9.7、常函数

1
2
3
4
5
6
7
8
class CStu
{
public:
void show() const
{
std::cout << "i am show" << std::endl;
}
};
  • 函数名() 后面增加一个const即为常函数
  • 构造函数和析构函数不能是常函数
  • 常函数不能修改数据成员,但是可以使用。这里的数据成员是指类内的数据成员,在函数中定义的数据成员可以被修改。
  • 常函数对函数的功能有更明确的限定
  • 长函数的this指针的数据类型是const CStu*
  • 常对象只能调用常函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class CStu
{
public:
void show() const
{
std::cout << "i am show" << std::endl;
}

void fun()
{
std::cout << "fun" << std::endl;
}
};

int main()
{
const CStu st1;
st1.fun(); //error
return 0;
}

上述代码中st1.fun();是错误的。st1是常对象,只能调用常函数

9.8、static

9.8.1、静态变量

静态成员属于类作用域,但不属于类对象。它的声明周期和普通的静态变量一样,程序运行时进行分配内存和初始化,程序结束时则被释放。所以不能在类的构造函数中进行初始化。

但是可以赋值!!!不要搞混

  • 静态成员无法通过初始化列表进行初始化
1
2
3
4
5
6
7
8
9
class CStu
{
public:
static int a; //error
CStu() : a(12)
{

}
};
  • 也无法在类内直接初始化
1
2
3
4
5
class CStu
{
public:
static int a = 12; //error
};
  • 但是可以通过构造函数赋值的方法进行初始化
1
2
3
4
5
6
7
8
9
class CStu
{
public:
static int a;
CStu()
{
a = 12;
}
};
  • 也可以在类外初始化
1
2
3
4
5
6
7
class CStu
{
public:
static int a;
};

int CStu::a = 12;//此处千万不要加static
  • 有一种比较特殊:静态常量整形数据成员可以在类内进行初始化
1
2
3
4
5
6
class CStu
{
public:
const static int a = 12;
//const static float b = 12.13f; //error
};

9.8.2、函数成员

1
2
3
4
5
6
7
8
9
10
11
12
class CStu
{
public:
static int a;
static void fun()
{
std::cout << a << std::endl;
}
};


int CStu::a = 13;

函数调用方法有两种

1
2
3
4
5
6
//1、类名作用域
CStu::fun();

//2、对象调用
CStu stu1;
stu1.fun();

9.8.3、注意

  • 静态成员没有this指针
  • 类内的静态函数不能调用普通的数据成员,只能调用静态成员

9.9、拷贝构造

1
2
3
4
5
6
7
8
9
10
11
12
13
class CStu
{
public:
CStu()
{

}

CStu(const CStu& a) //拷贝构造函数
{

}
};

9.9.1、何时调用?

  • 新建一个对象,并将其初始化为同类现有对象 CStu st1;

    1. CStu stNew(st1);
    2. CStu stNew = st1;
    3. CStu stNew = CStu(st1);
    4. CStu* stNew = new CStu();
  • 注意:使用对象进行赋值的时候不会调用拷贝构造函数

    1
    2
    CStu st2;
    st2 = st1;
  • 当程序生成对象副本时,调用拷贝构造函数

1
2
3
4
5
6
7
8
9
10
11
void fun(CStu a)
{

}

int main()
{
CStu st1;
fun(st1);
return 0;
}

在调用fun函数时会使用拷贝构造生成一个临时的对象

  • 做函数的返回值时
1
2
3
4
5
6
7
8
9
10
11
12
13
CStu fun()
{
CStu a;
return a;
}


int main()
{
CStu st1;
fun();
return 0;
}

注意,是在返回的时候调用拷贝构造函数

9.9.2、有何功能

默认的拷贝构造函数,逐个赋值非静态成员(成员的复制为浅复制)值,复制的是成员的值

9.9.3、浅拷贝的问题–指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class CStu
{
public:
int* a;
CStu()
{
a = new int[2];
a[0] = 12;
a[1] = 13;
}

~CStu()
{
delete[] a;
}
};

int main()
{
CStu stu;
std::cout << stu.a[0] << " " << stu.a[1] << std::endl;

CStu stNew = stu;
std::cout << stNew.a[0] << " " << stNew.a[1] << std::endl;
return 0;
}

这段程序可以编译通过,但是运行的时候会崩溃,这就是浅拷贝的问题,指针内存的释放问题

浅拷贝只是复制了指针的值,在释放st的时候a(stu.a)指针的内存就已经被释放,而再次释放stNew的时候,仍需要对a(stNew.a)进行释放,由于此前已经释放过一次,所以程序会发生崩溃。

9.4、深拷贝

深拷贝就是亲自实现拷贝构造方法

考虑到,对象释放的时候肯定会执行析构函数,而指针是每个Stu对象必不可少的内容,所以我们需要将每个对象的a指针都申请一段空间,这样在释放的时候就不会出现问题。

1
2
3
4
5
CStu(const CStu& b)
{
this->a = new int[2];
memcpy(this->a, b.a, 8);
}

这样的拷贝构造函数就可以实现每个Stu对象的a指针都有一段内存

9.5、内联函数

9.5.1、常规内联函数

常规函数调用需要根据函数地址跳转到函数的代码空间,然后执行指令

内联函数则是将函数的代码直接复制到函数调用的位置

二者的区别:

  • 比常规函数稍快
  • 代价是占用内存过多

内联函数声明的方式:

1
2
3
4
5
6
7
8
9
10
11
12
inline void fun();//声明

int main()
{

return 0;
}

inline void fun()//定义
{

}

函数声明与函数定义位置都需要添加inline

在使用内联函数的时候需要考虑内联函数的性价比问题。而实际上我们在使用内联函数的时候,编译器不一定会答应的。

9.5.2、类内的内联函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class CStu
{
public:
void fun() //inline 隐式定义
{

}

inline void fun1() // inline 显式定义
{

}

void fun2();

inline void fun3();

void fun4();
};

inline void CStu::fun2() //内联函数
{

}

void fun3()
{

}

void fun4()
{

}

在上述的四个函数中,fun1肯定是内联函数,fun4()肯定不是内联函数

fun2()与fun3()一个在类内声明,而定义没有inline;另一个声明没有inline而定义有inline,这两个函数是否为内联函数需要看编译器的执行情况。

9.5.3、注意

内联函数的声明与定义必须在一个文件中,即使在头文件中,也需要将定义写在头文件中,不然不会得到内联的效果


从C开始的CPP学习生活01-基础内容
http://example.com/2022/10/26/cppnote01/
作者
Anhongzhan
发布于
2022年10月26日
许可协议