C++语法

C++语法

c++ 函数后面加一个冒号

c++ 函数后面加一个冒号的含义

  • 赋值,冒号后面跟的是赋值,这种写法是C++的特性。
1
2
3
4
5
6
7
8
9
10
11
12
int a;
int b;
A( int aa, int bb ):a(aa),b(bb)
{

}
// 相当于
A( int aa, int bb )
{
a=aa;
b=bb;
}
  • 继承,比如class Derived : private Base ,冒号前面是派生类(子类),后面是基类(父类)。

C++ “::” 作用域符 双冒号

C++ “::” 作用域符 双冒号

左关联(left-associativity),作用都是为了更明确的调用想要的变量:

  • ==类作用域符==:class scope,作用域符号::的前面一般是类名称,后面一般是该类的成员名称

    A,B表示两个类,在A,B中都有成员member。那么
    A::member就表示类A中的成员member,B::member就表示类B中的成员member。

  • ==全局作用域符号==:global scope,当全局变量在局部函数中与其中某个变量重名,那么就可以用::来区分,如:

1
2
3
4
5
6
7
char zhou; //全局变量
void sleep()
{
char zhou; //局部变量
char(局部变量) = char(局部变量) *char(局部变量) ;
::char(全局变量) =::char(全局变量) *char(局部变量);
}
  • ==命名空间作用域==:namespace::name

    想调用namespace std中的cout成员,你就写成std::cout(相当于using namespace std;cout)意思是在这里我想用cout对象是命名空间std中的cout(即就是标准库里边的cout)

  • ==作用域分解运算符==

    声明了一个类A,类A里声明了一个成员函数voidf(),但没有在类的声明里给出f的定义,那么在类外定义f时,就要写成voidA::f(),表示这个f()函数是类A的成员函数。例如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class CA {
    public:
    int ca_var;
    int add(int a, int b);
    int add(int a);
    };

    //那么在实现这个函数时,必须这样书写:
    int CA::add(int a, int b)
    {
    return a + b;
    }

    //另外,双冒号也常常用于在类变量内部作为当前类实例的元素进行表示,比如:
    int CA::add(int a)
    {
    return a + ::ca_var;
    }
    //表示当前类实例中的变量ca_var。

c++ 类名后<>里面的内容代表什么?

c++ 类名后<>里面的内容代表什么?

  • 模板的参数

下面一个模板,TArgs前面那…有什么意义?
类名linq_lambda_retriver后<>里面的内容的应该如何理解?

1
2
3
4
5
template<typename TClass, typename TResult, typename ...TArgs>
struct linq_lambda_retriver<TResult(__thiscall TClass::*)(TArgs...)const>
{
typedef TResult ResultType;
};

在这里是模板linq_lambda_retriver特化的参数,而且是长度可变的模板参数
意思大概是 返回值类型是TResult 参数是TArgs...(这是参数包展开) 调用约定是__thiscallTClass的一个const 成员函数指针 类型

C++ 中using 的使用

C++ 中using 的使用

  • ==命名空间的使用==:

一般为了代码的冲突,都会用命名空间。例如,对于Android代码会使用Android作为命名空间。

namespace android;

在code中使用的时候可以用**android::**加具体的类方法。也可以直接使用using namespace android;

  • ==在子类中引用基类的成员==:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class T5Base {
public:
T5Base() :value(55) {}
virtual ~T5Base() {}
void test1() { cout << "T5Base test1..." << endl; }
protected:
int value;
};

class T5Derived : private T5Base {
public:
//using T5Base::test1;
//using T5Base::value;
void test2() { cout << "value is " << value << endl; }
};

基类中成员变量value是protected,在private继承之后,对于外界这个值为private,也就是说T5Derived的对象无法使用这个value。

如果想要通过对象使用,需要在public下通过using T5Base::value来引用,这样T5Derived的对象就可以直接使用。

同样的,对于基类中的成员函数test1(),在private继承后变为private,T5Derived的对象同样无法访问,通过using T5Base::test1 就可以使用了。

注意,using只是引用,不参与形参的指定。

  • ==别名指定==

在C++11中提出了通过using指定别名。

1
using value_type = _Ty

以后使用value_type value; 就代表_Ty value;

类似typedef

C++ this 指针 this->

C++ this 指针

在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。

友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。

举例:

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
35
36
37
38
39
40
41
42
43
44
#include <iostream>

using namespace std;

class Box
{
public:
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
}
double Volume()
{
return length * breadth * height;
}
int compare(Box box)
{
return this->Volume() > box.Volume();
}
private:
double length; // Length of a box
double breadth; // Breadth of a box
double height; // Height of a box
};

int main(void)
{
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box Box2(8.5, 6.0, 2.0); // Declare box2

if(Box1.compare(Box2))
{
cout << "Box2 is smaller than Box1" <<endl;
}
else
{
cout << "Box2 is equal to or larger than Box1" <<endl;
}
return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
2
3
Constructor called.
Constructor called.
Box2 is equal to or larger than Box1

再举例:

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
#include <iostream>
using namespace std;

class Box{
public:
Box(){;}
~Box(){;}
Box* get_address() //得到this的地址
{
return this;
}
};

int main(){

Box box1;
Box box2;
// Box* 定义指针p接受对象box的get_address()成员函数的返回值,并打印

Box* p = box1.get_address();
cout << p << endl;

p = box2.get_address();
cout << p << endl;

return 0;
}

this 指针的类型可理解为 **Box***。

此时得到两个地址分别为 box1 和 box2 对象的地址。

C++ Primer Page 258

引入 this:

当我们调用成员函数时,实际上是替某个对象调用它。

成员函数通过一个名为 this 的额外隐式参数来访问调用它的那个对象,当我们调用一个成员函数时,用请求该函数的对象地址初始化 this。例如,如果调用 total.isbn()则编译器负责把 total 的地址传递给 isbn 的隐式形参 this,可以等价地认为编译器将该调用重写成了以下形式:

1
2
//伪代码,用于说明调用成员函数的实际执行过程
Sales_data::isbn(&total)

其中,调用 Sales_data 的 isbn 成员时传入了 total 的地址。

在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点,因为 this 所指的正是这个对象。任何对类成员的直接访问都被看作是对 this 的隐式引用,也就是说,当 isbn 使用 bookNo 时,它隐式地使用 this 指向的成员,就像我们书写了 this->bookNo 一样。

对于我们来说,this 形参是隐式定义的。实际上,任何自定义名为 this 的参数或变量的行为都是非法的。我们可以在成员函数体内部使用 this,因此尽管没有必要,我们还是能把 isbn 定义成如下形式:

1
std::string isbn() const { return this->bookNo; }

因为 this 的目的总是指向“这个”对象,所以 this 是一个常量指针(参见2.4.2节,第56页),我们不允许改变 this 中保存的地址。

class

把类理解成“类型”,就如同int string一样,是一种类型(其实int是类型的关键字)

查看可执行文件依赖库

可执行文件a,查看它的依赖库:ldd a

指定库路径:

https://segmentfault.com/a/1190000016433897

export LD_LIBRARY_PATH=库:$LD_LIBRARY_PATH

c语言编译成.o文件

gcc -c mian.c

如果不加.c,直接编译成可执行文件(而不是object文件)

c++调用c接口

C++是如何调用C接口的?

在c的头文件写:

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
#ifdef __cplusplus
extern "C"{
#endif

void testCfun();

#ifdef __cplusplus
}
#endif

memcpy

memcpy(dst, src, len);

1
2
memcpy(input + reserved_frame_num * input_dim,
feature, sizeof(float)*input_frame_num*input_dim);

这里是把feature付给的地址是input + reserved_frame_num * input_dim,input[0]到input[reserved_frame_num * input_dim]位置之间保持不变,从(input + reserved_frame_num * input_dim)的地址开始复制。

1
2
3
4
#define 。。。

//如果想当前文件不启用这个
#undef 。。。

https://zh.cppreference.com/w/cpp/container/vector

https://qingcms.gitee.io/cppreference/20210212/zh/

新建数组:

1
2
3
4
5
6
vector<string> ans;	//初始值都是""
// vector<int> cnt(26); //初始值都是0
string s = "";

ans.push_back(s)
// 如果vector一开始规定了大小,可以直接: cnt[1]=100;

int转string

1
to_string(i)

vector长度:

1
s.size()

typedef

类型取一个新的名字,也可以使用 typedef 来为用户自定义的数据类型取一个新的名字,然后使用这个新的数据类型来直接定义结构变量。

下面的实例为单字节数字定义了一个术语 BYTE

1
typedef unsigned char BYTE;
1
2
3
4
5
6
7
typedef struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} Book;

#define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:

  • typedef 仅限于为类型定义符号名称,**#define** 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
  • typedef 是由编译器执行解释的,**#define** 语句是由预编译器进行处理的。

{}

作用域

1
2
3
4
5
6
7
nnet2::AmNnet nnet;
{
bool binary;
Input ki(nnet2_rxfilename, &binary);
trans_model.Read(ki.Stream(), binary);
nnet.Read(ki.Stream(), binary);
}

这里的binary只在{}内的作用域里有用,指令执行到}之后,binary这个变量就没了。

子类调用父类方法

https://developer.aliyun.com/article/857737

  1. 外部通过子类调用父类方法 : 如果调用的方法在子类中没有重写 , 那么调用的就是父类的方法 ;
  2. 子类中调用父类的方法 : 使用 “ 父类名称 :: 方法名() “ 进行调用
1
2
3
4
5
6
7
8
class Child : private Parent, public Parent1 {
public:
void parent_method() {
//子类中调用父类的方法 , 该操作相当于 Java 中的 super 方法
Parent::parent_method();
Parent1::parent_method();
cout << " Child parent_method " << endl;
}

父类调用子类方法

利用函数指针实现父类函数调用子类函数

方法1:

新建的对象是”一个指向子类的父类指针或引用“

利用多态机制,一个指向子类的父类指针或引用,当调用被子类重写的虚函数vritual时,实际上调用的是子类函数,这是通过多态的方式来实现父类调用子类,该方法需要一个引用或者指针调用虚函数来实现。

我的理解:父类里面有一个方法/函数 命名是 virtual 虚函数的,没有实现,而在子类的头文件(.h)也有这个虚函数,子类的实现文件(.cc)里面有实现过程(代码里不用添加virtual了),因此就可以父类调用子类。

如下面所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Class Base
{
public:
virtual void fun()
{
std::cout << "Base::fun()" << std::endl;
}
};

class Derived : public Base
{
public:
virtual void fun()
{
std::cout << "Derived::fun()" << std::endl;
}
};

Base* base = new Derived;
base->fun(); //该结果输出为: "Derived::fun()"

kaldi实例:==【这个写法要记下来】==

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
// online2/online-nnet2-feature-pipeline.h
OnlineBaseFeature *base_feature_;
// online2/online-nnet2-feature-pipeline.cc
base_feature_->AcceptWaveform(sampling_rate, waveform);

// itf/online-feature-itf.h
class OnlineBaseFeature: public OnlineFeatureInterface {
public:
virtual void AcceptWaveform(BaseFloat sampling_rate,
const VectorBase<BaseFloat> &waveform) = 0;}

//feat/online-feature.h
class OnlineGenericBaseFeature: public OnlineBaseFeature {
public:
virtual void AcceptWaveform(BaseFloat sampling_rate,
const VectorBase<BaseFloat> &waveform);}

// feat/online-feature.cc
template <class C>
void OnlineGenericBaseFeature<C>::AcceptWaveform(
BaseFloat sampling_rate, const VectorBase<BaseFloat> &original_waveform) {
//...具体实现
}

//可以看出,子类是OnlineGenericBaseFeature,父类是OnlineBaseFeature

// 这个写法很经典,可以背下来!

方法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
35
36
37
38
39
40
41
class Base
{
public:
typedef void (Base::*pfn)();

void CallFunction()
{
if(fn != NULL)
{
(this->*fn)(); //调用Derived类中的fun()函数
}
}
void SetPfn(pfn fn)
{
this->fn = fn;
}

private:
pfn fn;

};

class Derived : public Base
{
public:
void Init()
{
SetPfn((pfn)&Derived::fun);
}

void fun()
{
std::cout << "Derived::fun be called in Base function!" << std::endl;
}

};

Derived derived;
derived.Init();
derived.CallFunction();//这里调用的是父类的成员函数,该函数通过函数指针调用了子类的普通成员函数,调用结果输出:"Derived::fun be called in Base function!",即父类成员函数调用了子类成员函数。

C++函数什么时候要加&

chatgpt

在 C++ 中,函数声明和定义时需要加 & 的情况如下:

  1. 当函数返回类型为引用时,函数名前需要加 &。
  2. 当函数作为另一个函数的参数时,如果需要通过引用来操作这个参数,则需要在参数前加 &。

加 & 的目的是提高程序的效率,因为通过引用访问变量不需要在内存中复制,而是直接使用原变量的地址。

C++中,当一个函数返回值为一个非基本数据类型的对象时,通常需要使用引用(&)修饰符。为了防止复制对象带来的额外的开销和性能问题,将对象的地址作为引用传递给函数,这样就可以避免复制了。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

class Test
{
public:
Test(int a, int b) : x(a), y(b) {}
int x;
int y;
};

Test& GetTestObject(int a, int b)
{
Test t(a, b);
return t;
}

int main()
{
Test &t = GetTestObject(10, 20);
cout << t.x << " " << t.y << endl;
return 0;
}

在上面的代码中,函数 GetTestObject 返回一个 Test 类型的对象,因此需要加上 & 修饰符。

C++函数返回类型为引用时,函数名前需要加 &的例子:

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

int& getMax(int& a, int& b) {
return (a > b) ? a : b;
}

int main() {
int x = 5, y = 10;
int& max = getMax(x, y);
std::cout << "Max value: " << max << std::endl;
return 0;
}

在这里,getMax函数返回了一个整数引用。通过将函数返回值绑定到变量max,返回的是a或b的引用,而不是一个副本。这使得max变量可以直接修改a或b的值,并且在输出Max value时,打印的是最大值。

C++传参时什么时候要加&

C++ 中,在传递参数时要加 & 符号,当且仅当您希望通过函数对参数进行修改并在调用函数后在调用者中反映这些修改时。

当传递非常大的对象(例如数组或结构)时,传递引用可以避免复制完整的对象,从而节省内存和时间。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;

void swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}

int main()
{
int x = 5, y = 10;
cout<< "Before swap: x = "<< x << ", y = "<< y << endl;
swap(x, y);
cout<< "After swap: x = "<< x << ", y = "<< y << endl;
return 0;
}

在上面的代码中,swap函数通过加 & 对函数的参数进行修改,并在主函数中反映这些修改。

在 C++ 中,函数声明/定义什么时候要加&

在 C++ 中,当函数返回类型为引用时,函数声明/定义前需要加 &。

例如:

1
int& max(int &x, int &y);

此函数返回两个参数中的最大值,并且返回的是引用类型,函数 max 返回了 int 类型的引用。

在 C++ 中,如果函数的返回值是指针或者引用类型,函数名前不需要加 &。

当函数的参数为指针或者引用类型时,为了明确指出参数的传递方式,我们也可以加 &。??

C++函数什么时候要加*

在 C++ 中,当一个函数返回指针类型时,函数声明前需要加一个 * 号。这标志着函数返回了一个指向某个对象的指针,而不是返回该对象本身。例如:

1
int *getPointer();

该函数声明了一个返回 int 类型指针的函数。

在 C++ 中,函数声明/定义什么时候要加*

在 C++ 中,当声明/定义一个函数返回值(返回类型)为指针时,通常需要在函数名前加 *。例如:

1
int *getData();

这个函数声明表明函数返回一个整型指针。函数名前需要加一个星号,因为该函数返回一个 int 类型的指针。

C++传参什么时候要加*

在 C++ 中,函数参数要加*,当该参数是一个指针类型时需要加星号。

例如:

1
2
3
4
5
6
7
void printArray(int *array, int size)
{
for (int i = 0; i < size; i++)
{
cout << array[i] << " ";
}
}

在上面的代码中,第一个参数是一个整型指针,所以需要加*号。

C++传参什么时候用&,什么时候用*?

在 C++ 中,参数的传递方式主要有两种:值传递和引用传递。

  • 当函数需要修改参数的值并且希望在函数外也生效时,使用引用传递。引用传递通过在形参前加 & 实现,例如:void swap(int &a, int &b)。
  • 当函数仅仅需要读取参数的值并不需要修改时,使用值传递。值传递通过直接在形参写出变量类型和名称实现,例如:int max(int a, int b)。值传递的实现是复制了一份。

当函数需要处理指针时,可以使用指针参数。指针参数通过在形参前加 * 实现,例如:void swap(int *a, int *b)。

总的来说,需要根据函数的具体需求,确定是使用值传递、引用传递还是指针传递

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;
// 引用传递
void foo1(int& ptr) {
// code here
std::cout << ptr << std::endl;
}
// 指针传递
void foo2(int *ptr) {
// code here
std::cout << *ptr << std::endl;
}
int main(){
int x = 42;

foo1(x); //传进去变量,但是用的时候是引用
foo2(&x); //传进去地址

return 0;
}

C++头文件怎么写?

chatgpt

在C++中,头文件通常包含在两个尖括号(<>)或一对双引号(””)中,并放在源代码文件的开头。通常包括函数和变量的声明、宏定义、结构体和类的定义等。以下是一个头文件的示例:

1
2
3
4
5
6
7
8
9
#ifndef MY_HEADER_FILE_H // 常用的头文件保护宏
#define MY_HEADER_FILE_H

// 头文件内容
#include <iostream>

void myFunction(); // 函数声明

#endif // MY_HEADER_FILE_H

这个示例中的头文件名为”my_header_file.h”,头文件保护宏可以避免重复包含同一个头文件。头文件中包含了一个函数的声明。在源代码文件中,可以使用#include指令来包含头文件,例如#include "my_header_file.h"

常量指针、指针常量

csdn:【C++基础之二】常量指针和指针常量

常量指针

定义:具有只能够读取内存中数据,却不能够修改内存中数据的属性的指针,称为指向常量的指针,简称常量指针。

声明:

1
2
const int * p; 
int const * p;

注:可以将一个常量的地址赋值给一个对应类型的常量指针,因为常量指针不能够通过指针修改内存数据。只能防止通过指针引用修改内存中的数据,并不保护指针所指向的对象。

指针常量

定义:指针常量是指指针所指向的位置不能改变,即指针本身是一个常量,但是指针所指向的内容可以改变。

声明:

1
int * const p=&a;

注:指针常量必须在声明的同时对其初始化,不允许先声明一个指针常量随后再对其赋值,这和声明一般的常量是一样的。

举例:

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
35
36
37
38
39
40
41
42
43
int _tmain(int argc, _TCHAR* argv[])
{
//定义变量
int a=1;

//定义常量
const int b=2;

//定义常量指针
const int *ptr1=&a;

//定义指针常量,必须赋值
int* const ptr2=&a;

//错误,不能把常量的地址赋给指针变量
int *ptr3=&b;

//正确,可以把常量的地址赋给常量指针
const int* ptr4=&b;

//错误,间接引用常量指针不可以修改内存中的数据
*ptr1=3;

//正确,间接引用指针常量可以修改内存中的数据
*ptr2=4;

//正确,常量指针可以指向其他变量
ptr1=&b;

//错误,指针常量不可以指向其他变量
ptr2=&b;

//常量指针常量,即不可以间接引用修改内存数据,也不可以指向别的变量
const int * const ptr5=&a;

//错误,不可以间接引用修改内存数据
*ptr5=5;

//错误,不可以修改指向的对象
ptr5=&b;

return 0;
}

const int * p和int const * p;是一样的,都是常量指针。以*为中间划一条线 |,看const修饰谁就谁就是常量。int *const p=&a 是指针常量。

也可以理解为 左定值,右定向:

const在*号左边,表示指针指向的内容不能修改(常量指针)
const在*号右边,表示指针指向的地址不能修改(指针常量)

C++ const

chatgpt

在C++中,const是用来修饰变量、函数、指针等的关键字,它表示它所修饰的内容是只读的(即不能被修改)。具体来说:

  1. 对于变量,const可以用来修饰任何类型的变量,例如:

    1
    2
    const int x = 10; // 定义一个只读的整数变量 x,不允许对其进行修改
    const double pi = 3.14159; // 定义一个只读的浮点数常量 pi,不允许对其进行修改
  2. 对于函数,const可以用来修饰成员函数,表示这个成员函数不会修改类的成员变量,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    class MyClass {
    public:
    void func() const; // 声明一个只读的成员函数 func,不允许修改类的成员变量
    };

    void MyClass::func() const {
    // 在这里不允许修改类的成员变量
    }

    举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <iostream>
    using namespace std;
    class a{
    public:
    int aa = 1;
    const int b = 10;
    void bb() const;
    };

    void a::bb() const {
    // a::aa = 2; // 不可以
    cout << a::aa;
    }

    int main(){
    a aa;
    aa.bb();
    aa.aa = 2; // 可以
    // aa.b++; //不可以
    return 0;
    }
  3. 对于指针,const可以用来修饰指针本身或指针所指向的内容,例如:

    1
    2
    3
    const int* ptr1; // 定义一个指向只读整数的指针,指针本身可以修改,但指向的内容不可修改
    int* const ptr2; // 定义一个只读的指针,指针本身不可修改,但指向的内容可以修改
    const int* const ptr3; // 定义一个只读的指针,指针本身和指向的内容都不可修改

使用const有以下好处:

  1. 可以帮助程序员更好地防止程序中出现不必要的变量修改,提高程序的安全性和可靠性。
  2. 可以让编译器进行更多的优化,提高程序的性能。
  3. 可以帮助程序员更好地理解代码,提高代码的可读性和可维护性。

C++ 二级指针

chatgpt:C++中两个星号**指的是什么?

在 C++ 中,两个星号 ** 是指针的指针,也称为二级指针。它表示一个指向指针的指针变量。

例如,以下代码定义了一个指向指针的指针 pp

1
2
3
int a = 10;
int* p = &a;
int** pp = &p;

在这个例子中,p 是一个指向 a 变量的指针,pp 是一个指向 p 指针的指针。可以通过 pp 指针来修改 p 指针指向的变量:

1
2
int b = 20;
*pp = &b; // 现在 p 指向 b // *pp想成是p,于是 p = &b,也就是p指向b的地址(不再指向a的地址了)

在这个例子中,*pp 取得了 p 的值,即指向变量 a 的指针,然后将其赋值为指向变量 b 的指针,因此现在 p 指向了变量 b

chatgpt:什么时候会用到二级指针?

在C++或C语言中,二级指针是指一个指向指针的指针。在以下情况下,可能会用到二级指针:

  1. 函数需要修改指针所指向的内存地址:如果一个函数需要修改一个指针所指向的内存地址,但是这个指针本身是一个函数参数,那么需要传递一个指向指针的指针作为参数,以便在函数内部能够修改指针指向的内存地址。
  2. 多级指针数据结构:有些数据结构,如链表或树,可能需要使用二级指针来访问和修改数据。在这种情况下,一个指针存储了一个指向另一个指针的指针,以便可以轻松访问和操作数据结构。
  3. 动态内存分配:在动态内存分配过程中,可能需要使用二级指针来分配和释放内存。这是因为动态分配内存时,需要使用指向指针的指针来分配和释放内存,以便正确地管理内存。

chatgpt:不同情况使用二级指针举例

下面是一些可能会使用二级指针的具体情况:

  1. 函数需要修改指针所指向的内存地址:假设有一个函数需要修改一个整型指针指向的内存地址,而这个指针本身是一个函数参数,可以使用如下代码:
1
2
3
4
5
void modifyPointer(int **ptr) {
int *temp = new int;
*temp = 10;
*ptr = temp;
}

在上面的例子中,modifyPointer 函数使用了一个指向指针的指针 int **,并且在函数内部创建了一个新的整型指针,将它的值设置为 10,并将这个新指针赋值给传递进来的指针。这样就可以修改指针所指向的内存地址了。

  1. 多级指针数据结构:假设有一个链表数据结构,其中每个节点都是一个指向下一个节点的指针。这个数据结构可以使用二级指针来操作。例如,可以使用以下代码向链表中添加一个新的节点:
1
2
3
4
5
6
7
8
9
10
11
struct Node {
int data;
Node *next;
};

void addNode(Node **head, int newData) {
Node *newNode = new Node;
newNode->data = newData;
newNode->next = *head;
*head = newNode;
}

在上面的例子中,addNode 函数使用了一个指向指针的指针 Node **,并在函数内部创建了一个新的节点 newNode。然后将 newNodenext 指针设置为传递进来的指针所指向的节点,并将 newNode 赋值给传递进来的指针。这样就可以向链表中添加一个新的节点了。

  1. 动态内存分配:假设需要动态分配一个二维数组,可以使用二级指针来管理内存。例如,可以使用以下代码动态分配一个 3 行 4 列的二维数组:
1
2
3
4
int **array = new int*[3];
for (int i = 0; i < 3; i++) {
array[i] = new int[4];
}

在上面的例子中,首先动态分配一个 int* 类型的数组,大小为 3,然后在循环中动态分配每个元素的指针,大小为 4。这样就可以动态分配一个 3 行 4 列的二维数组了。

explicit

chatgpt

在 C++ 中,explicit 是一个关键字,用于修饰构造函数或者转换函数,其目的是防止编译器进行隐式类型转换

当一个类有一个只带一个参数的构造函数时,如果没有使用 explicit 修饰,那么编译器会自动将该构造函数用于将一个参数的表达式转换为该类类型的对象,这被称为隐式类型转换。然而,这种自动转换可能会导致不期望的行为和错误,因此可以使用 explicit 关键字来防止它。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyInt {
public:
MyInt(int value) : value_(value) {}

private:
int value_;
};

void func(MyInt my_int) {}

int main() {
func(123); // 这里会进行隐式类型转换,编译器会将 123 转换为 MyInt 类型
return 0;
}

如果不使用 explicit,编译器会将 123 隐式转换为 MyInt 类型,然后传递给 func 函数。但是,如果使用 explicit 关键字来修饰 MyInt 的构造函数,那么上述代码就会编译失败,因为不能隐式地将 int 类型转换为 MyInt 类型。

https://zh.cppreference.com/w/cpp/language/explicit

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
struct A
{
A(int) { } // 转换构造函数
A(int, int) { } // 转换构造函数(C++11)
operator bool() const { return true; }
};

struct B
{
explicit B(int) { }
explicit B(int, int) { }
explicit operator bool() const { return true; }
};

int main()
{
A a1 = 1; // OK:复制初始化选择 A::A(int)
A a2(2); // OK:直接初始化选择 A::A(int)
A a3 {4, 5}; // OK:直接列表初始化选择 A::A(int, int)
A a4 = {4, 5}; // OK:复制列表初始化选择 A::A(int, int)
A a5 = (A)1; // OK:显式转型进行 static_cast
if (a1) ; // OK:A::operator bool()
bool na1 = a1; // OK:复制初始化选择 A::operator bool()
bool na2 = static_cast<bool>(a1); // OK:static_cast 进行直接初始化

// B b1 = 1; // 错误:复制初始化不考虑 B::B(int)
B b2(2); // OK:直接初始化选择 B::B(int)
B b3 {4, 5}; // OK:直接列表初始化选择 B::B(int, int)
// B b4 = {4, 5}; // 错误:复制列表初始化不考虑 B::B(int,int)
B b5 = (B)1; // OK:显式转型进行 static_cast
if (b2) ; // OK:B::operator bool()
// bool nb1 = b2; // 错误:复制初始化不考虑 B::operator bool()
bool nb2 = static_cast<bool>(b2); // OK:static_cast 进行直接初始化
}

https://stackoverflow.com/questions/121162/what-does-the-explicit-keyword-mean

https://c-cpp.com/cpp/language/explicit

std::move

https://zh.cppreference.com/w/cpp/utility/move

csdn:c++ 之 std::move 原理实现与用法总结

功能将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。

从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);

  1. C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。
  2. std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能.。
  3. 对指针类型的标准库对象并不需要这么做.

用法:

原lvalue值被moved from之后值被转移,所以为空字符串.

std::move 的函数原型定义

1
2
3
4
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);

举例:

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
#include <iostream>
#include <utility>
#include <vector>
#include <string>

int main()
{
std::string str = "Hello";
std::vector<std::string> v;

// 使用 push_back(const T&) 重载,
// 表示我们将带来复制 str 的成本
v.push_back(str);
std::cout << "After copy, str is \"" << str << "\"\n";

// 使用右值引用 push_back(T&&) 重载,
// 表示不复制字符串;而是
// str 的内容被移动进 vector
// 这个开销比较低,但也意味着 str 现在可能为空。
v.push_back(std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";

std::cout << "The contents of the vector are \"" << v[0]
<< "\", \"" << v[1] << "\"\n";
}

/*输出
After copy, str is "Hello"
After move, str is ""
The contents of the vector are "Hello", "Hello"
*/

左值、左值引用、右值、右值引用

博客园:左值、左值引用、右值、右值引用

1、左值和右值的概念

​ 左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;
​ 右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。
​ 一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址

2、引用

​ 引用是C++语法做的优化,引用的本质还是靠指针来实现的。引用相当于变量的别名。

​ 引用可以改变指针的指向,还可以改变指针所指向的值。

​ 引用的基本规则:

  1. 声明引用的时候必须初始化,且一旦绑定,不可把引用绑定到其他对象;即引用必须初始化,不能对引用重定义
  2. 对引用的一切操作,就相当于对原对象的操作。

3、左值引用和右值引用

3.1 左值引用
左值引用的基本语法:type &引用名 = 左值表达式;

3.2 右值引用

​ 右值引用的基本语法type &&引用名 = 右值表达式;

​ 右值引用在企业开发人员在代码优化方面会经常用到。

​ 右值引用的“&&”中间不可以有空格。

函数对象 operator()

csdn C++函数对象operator()

函数对象:定义了调用操作符()的类对象。当用该对象调用此操作符时,其表现形式如同普通函数调用一般。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
class A
{
public:
void operator()()
{
printf("1");
}
};

int main(){
A a;
a();
return 0;
}

// 输出 1

有参数的话:

1
2
3
4
5

int operator() ( int val ) //把参数写在第二个括号里,有个单独的括号在operator后面
{
return val > 0 ? val : -val;
}

因为有构造函数而可以直接传值的情况

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
struct Edge {
int to; // 边的目的地
int weight; // 边的权重
Edge(int t, int w) : to(t), weight(w) {}
};

class Graph {
private:
XXX adj_list; // 比如 vector<vector<Edge>> adj_list;
public:
Graph(int n) : adj_list(n) {}
};

int main() {
vector<Graph> b;
b.emplace_back(1);
/* 这样相当于传了一个Graph类的对象进去,等价于:
Graph bb(1);
b.emplace_back(bb); */

vector<Edge> a;
a.emplace_back(1,2);
/* 这样相当于传了一个Edge结构体的对象进去,等价于:
Edge aa(1,2);
a.emplace_back(aa);
*/
}