Why use int** ar and not int* ar? #include <stdio.h>
#include <stdlib.h>
void input(int **ar, int *n) {
    printf("Number of elements: ");
    scanf("%d", n);
    *ar = (int*)malloc(*n * sizeof(int));
    //*ar = new int[*n];
    for (int i = 0; i < *n; ++i){
printf("Element %d:", i);
scanf("%d", &(*ar)[i]);
    }
    return;
}
void output(int *ar, int n) {
    for (int i = 0; i < n; ++i) {
printf("%5d\n", ar[i]);
    }
    return;
}
int main() {
    int n = 0;
    int *ar = NULL;
    input(&ar, &n);
    output(ar, n);
    free(ar);
    return 0;
}

So I have just learned about pointers in C at my university.  Some of it I understand and some I don't.  For example, I don't understand why ar in the input function is an int** and not an int*.  I mean I try to replace int** ar with int* ar and delete the * in int* ar = NULL in the main function and there's no compile error.  When I run the program, everything works fine.  I enter the number of elements and enter some values in those elements but, when I enter the last number in the last element, I get nothing. So I undid everything and the program works normally again.

The program you have posted is doing following (in the given sequence below):

*

*Declare/define a pointer in a function

*Allocate memory to that pointer in some other function and fill that memory with user input

*Print the content of that memory using another function

*Deallocate memory and exit

The part which is confusing you is 2.

I don't understand why ar in the input function is an int** and not an int* ....

If you want to allocate memory to a pointer (or want to change the pointer, like, e.g. making it point to some other memory or reallocating memory etc.) in some other function, you need access of that pointer in that function and you can get access if you have the address of that pointer.
And that's why here the address of ar pointer is passed:
input(&ar, &n);

Pass address of ar to input() function and in the input function dereference that address (i.e. *ar) will give the ar pointer (declared/defined in main()).
For better understanding, I am renaming the ar parameter of input() function to input_ar:
void input(int *input_ar, int *n) {
    .....
    .....

When input() function called from main() function, the in-memory view would be something like this:
    input_ar       ar
      +-----+       +-----+      
      | 200 | ----> | NULL|
      +-----+       +-----+

[200 is address of ar pointer]

In input() function, when you do:
*input_ar = (int*)malloc(*n * sizeof(int));

dereferencing input_ar (i.e. *input_ar) will give ar:
    input_ar      *input_ar (ar)       (newly allocated memory block)
      +-----++-----+       +-----+
      | 200 | > | 300 | ----> |     |
      +-----++-----+       +-----+

[300 is address of the newly allocated memory block]

Hence, by dereferncing input_ar, the pointer ar can be changed in input().

I mean I try to replace int** ar with int* ar and delete the * in int* ar = NULL in the main function and there's no compile error.

Note that if you remove the * from declaration of ar in main() then ar will be a variable of type int, that means, it will not be a pointer but a variable which can hold a int type value. If you do this change in your program, you have to make changes in other places as well like, the way you are taking input from user in input() function, respective changes in output() function etc.
You have not shown the program with the changes that you have done after deleting * from ar declaration so, I will just talk about the ar argument passed to input():
This is what you might have tried:
int ar;

input (&ar, &n);

and in input():
void input(int *ar, int *n) {
    printf("Number of elements: ");
    scanf("%d", n);
    ar = (int*)malloc(*n * sizeof(int));

    .....

Note that the ar pointer parameter is a local variable of input() function.
When input() function is called from main() function, the ar will hold the address of ar variable of main() function.
If you want, you can make changes in the value of ar variable using  the pointer ar in input() function, e.g. - *ar = 9;
This will assign value 9 to variable ar (of main()). The in-memory view would be something like this
    ar (of input())ar (of main())
      +-----++-----+      
      | 200 | > |     |
      +-----++-----+

[200 is address of ar variable of main()]

But when you do
ar = (int*)malloc(*n * sizeof(int));

the ar has lost the address of ar variable of main() function and newly allocted memory reference is assigned to pointer ar of input(), i.e.
    ar (of input())newly allocated memory block
      +-----++-----+      
      | 400 | > |     |
      +-----++-----+

[400 is address of the newly allocated memory block]

You can write/modify the contents in this memory but when returned from input(), this memory reference will be lost because input() is not returning anything and this will be a memory leak in your program.

A common use of pointers, as they're being used here, is to allow a function call to change the value of one of your variables.
For example, suppose you had the following:
void
change_num(int num)
{
    num = 6;
}

int
main()
{
    int num = 5;
    change_num(num);
    printf("%i\n", num);

    return 0;
}

This doesn't work because change_num is changing its own copy of num and not num itself.
Instead, you have to pass a pointer to num to the function:
void
change_num(int* num_ptr)
{
    *num_ptr = 6;
}

int
main()
{
    int num = 5;

    change_num(&num);
    printf("%i\n", num);

    return 0;
}

The line *num_ptr = 6; means, "Change the object pointed to by num_ptr to 6."
In the general case, if we want a function to change the value of a variable of type SomeType, then we must pass a SomeType* to the function.
In your case, you want to change the value of ar, which is an int*.  Right now, it doesn't hold the address of anything (NULL) and you want it to be changed to hold the value of an allocated array (created by malloc).  If we pass an int* to a function in order to change an int and we pass a SomeType* in order to change a SomeType, then we must pass an int** in order to change an int*.