Qual é a melhor forma de utilizar ponteiros? [RESOLVIDO]

1. Qual é a melhor forma de utilizar ponteiros? [RESOLVIDO]

J4ne D0bbs
j4ned0bbs

(usa Slackware)

Enviado em 08/03/2026 - 17:01h

Eu sei, é uma pergunta meio boba mas eu venho me questionando.

Estive estudando recentemente C por que eu gosto bastante de C mas bem no inicio estudando sobre ponteiros eu fiquei me perguntando qual é a melhor aplicação para se usar ponteiros e para que eles ajudariam/serviriam.

Peguem leve ksksk, eu ainda sou meio leiga sobre programação de "baixo nivel" e ainda to estudando quando posso.


  


2. MELHOR RESPOSTA

Gustavo Samuel Bacagine Azevedo
Bacagine

(usa Arch Linux)

Enviado em 09/03/2026 - 18:34h

j4ned0bbs escreveu:


Bacagine escreveu:

Sou desenvolvedor C há 7 anos, e em todos esses anos, duas maneiras de usar ponteiros me chamaram atenção.

Os conceitos a seguir são avançados, algumas coisas que vou falar não são abordadas muito em livros ou em cursos por aí, então não se preocupe se tiver dificuldades em entender.

1) Ponteiros Genéricos (void*)
Os ponteiros genéricos (void pointers) são usados quando você não sabe qual tipo de dado vai ser usado. Imagine que você cria uma estrutura de lista, e ao invés de definir qual dado será salvo na lista, você cria uma lista com ponteiro genérico, assim ela passa a aceitar qualquer tipo de dado.


typedef struct STRUCT_GENERIC_LIST {
void* pData;
struct STRUCT_GENERIC_LIST* pstNext;
} STRUCT_GENERIC_LIST, *STRUCT_GENERIC_LIST;


2) Callbacks (ponteiros para funções)
Esse é um dos usos que eu mais vejo no dia a dia, trabalhando como desenvolvedor C na empresa onde estou.
Imagine que você está desenvolvendo uma biblioteca que tem uma função que precisa chamar outra, porém você não sabe o conteúdo dessa função que você vai chamar, você não tem essa função no seu código, a única coisa que você sabe é a assinatura que essa função tem. Nesse caso, você cria uma callback, um ponteiro para a função, assim quando um desenvolvedor usar sua biblioteca, ele irá passar a função como parâmetro para este ponteiro.
Veja um exemplo a seguir:


typedef int (*PFNCALLBACK)(void);
void vMyFunction(PFNCALLBACK pfnCallback);


1) Criamos um tipo de dado que é um ponteiro de função chamado PFNCALLBACK.
2) Criamos uma função que recebe como parâmetro uma função com a assinatura de PFNCALLBACK, ou seja, uma função que retorna um inteiro e não recebe parâmetros.

O desenvolvedor que for chamar a função vMyFunction terá que passar como parâmetro uma função que tenha essa assinatura. Exemplo:


int iHelloWorld(void) {
puts("Hello World!!!");
puts("I'm a callback :)");
return 1;
}

int main(void) {
vMyFunction(iHelloWorld);
return 0;
}


Caraca, essa ultima parte sobre Callbacks explodiu a minha cabeça! É genial.

Obrigada pela sua resposta eu gostei bastante e abriu a minha mente para os ponteiros. Só fiquei com pequenas duvidas e curiosidades:
Minha primeira duvida é o quanto um dado "Void" ocupa de bytes de memoria, por curiosidade e também por que eu queria entender como o compilador vai aceitar outros tipos de dados naquele ponteiro Void, como inteiros e caracteres sendo que eles tem tamanhos maiores e diferentes.
Outra curiosidade é, se me permite perguntar, como é trabalhar com C? Eu fiquei curiosa por que achei que empregos de programação em C fossem sumir por conta daquele borborinho todo de "linguagens vulneráveis".

Please, keep it simple!


Respondendo a sua primeira pergunta:

Um ponteiro genérico irá usar a mesma quantidade de bytes que qualquer ponteiro, no caso, 8 bytes. Para ver isso, você pode fazer o seguinte:

printf("void* tem tamanho de %ld bytes\n", sizeof(void*));


Ao manipular o ponteiro genérico, você precisa dizer qual tipo de dado ele é no momento da manipulação, você fará isso por meio de casting.

Vou dar um exemplo mais detalhado de ponteiros genéricos para você entender melhor na prática o uso, vou fazer algo simples, pois o uso disso pode ser muito mais complexo do que irei mostrar a seguir.

Imagine que temos um software com algumas estruturas, e queremos uma função para mostrar o conteúdo dessas estruturas. Porém, não queremos uma função para cada estrutura, mas sim uma única função que irá mostrar o conteúdo da estrutura passada como parâmetro, independente de qual seja a estrutura passada.

Nós faríamos o seguinte:


#include <stdio.h>
#include <string.h>

typedef enum ENUM_STRUCT_TYPE {
TYPE_PERSON,
TYPE_CAT
} ENUM_STRUCT_TYPE;

typedef struct STRUCT_PERSON {
char szName[64];
int iAge;
char szMsg[32];
} STRUCT_PERSON, *PSTRUCT_PERSON;

typedef struct STRUCT_CAT {
char szName[64];
int iAge;
char szMsg[32];
} STRUCT_CAT, *PSTRUCT_CAT;

void vPrintInfo(void* pData, ENUM_STRUCT_TYPE eStructType) {
switch ( eStructType ) {
case TYPE_PERSON: {
PSTRUCT_PERSON pstPerson = (PSTRUCT_PERSON) pData;
printf("============== PERSON ==============\n");
printf(
"Person name...: %s\n"
"Person age....: %d\n"
"Person Message: %s\n",
pstPerson->szName,
pstPerson->iAge,
pstPerson->szMsg
);
printf("====================================\n");
break;
}
case TYPE_CAT: {
PSTRUCT_CAT pstCat = (PSTRUCT_CAT) pData;
printf("============== CAT ==============\n");
printf(
"Cat name...: %s\n"
"Cat age....: %d\n"
"Cat Message: %s\n",
pstCat->szName,
pstCat->iAge,
pstCat->szMsg
);
printf("=================================\n");
break;
}
default: {
fprintf(stderr, "E: Invalid struture type!\n");
break;
}
}
}

int main(void) {
STRUCT_PERSON stPerson;
STRUCT_CAT stCat;

memset(&stPerson, 0x00, sizeof(stPerson));
memset(&stCat , 0x00, sizeof(stCat ));

snprintf(stPerson.szName, sizeof(stPerson.szName), "Gustavo Bacagine");
stPerson.iAge = 26;
snprintf(stPerson.szMsg, sizeof(stPerson.szMsg), "Hi, I'm C developer :)");

snprintf(stCat.szName, sizeof(stCat.szName), "Tigris");
stCat.iAge = 3;
snprintf(stCat.szMsg, sizeof(stCat.szMsg), "Miau");

vPrintInfo(&stPerson, TYPE_PERSON);
vPrintInfo(&stCat, TYPE_CAT);
vPrintInfo(&stCat, -1);

return 0;
}


A saída esperada seria:

============== PERSON ==============
Person name...: Gustavo Bacagine
Person age....: 26
Person Message: Hi, I'm C developer :)
====================================
============== CAT ==============
Cat name...: Tigris
Cat age....: 3
Cat Message: Miau
=================================
E: Invalid struture type!


Para seu aprendizado, gostaria de deixar como desafio que você reescreva o código acima, mas ao invés de usar ponteiro genérico, use callbacks.

Agora respondendo à sua segunda pergunta (lembrando que vou falar com base na minha experiência pessoal):

Atualmente, no Brasil, vejo dois cenários onde a linguagem C é dominante:

1) Sistemas Embarcados

2) Varejo (este é o ramo onde trabalho)

Sou desenvolvedor em uma software house que desenvolve software para supermercados, farmácias, postos de combustíveis, etc.

Eu trabalho na equipe responsável pela parte do PDV (o software que roda nos computadores onde tem o caixa do mercado ou nos self checkouts).

Esses softwares demandam extrema rapidez e baixo uso de memória. Exemplo: ao passar um item no leitor de código de barras, é necessário que o software busque as informações na base de dados e exiba todas as informações do item para o usuário do sistema em questão, às vezes em menos de um segundo. A linguagem C é o ideal para esse tipo de situação.

Muitas das empresas que desenvolvem software na área usam linguagens como C ou C++, a empresa onde trabalho utiliza apenas C, mas conheci outras empresas que usam C++.

Pode ser que existam empresas na área do varejo que usem outras linguagens, mas percebo que a linguagem C é a mais usada.

Como a empresa em que trabalho desenvolve o software há mais de vinte anos, dificilmente eles iriam migrar o software da linguagem C para uma nova linguagem.

Trabalhar com linguagem C é algo que traz muitos benefícios devido à velocidade da linguagem, porém, é necessário um cuidado maior ao desenvolver para evitar vazamento de memória, buffer overflow, etc.

Os riscos existem, e é necessário cautela em muitos casos, principalmente quando se trata de manipulação de memória.

Uma vez eu atendi um chamado onde o software morria por falha de segmentação. Após algumas horas analisando logs da aplicação e conversando com outros desenvolvedores, percebi que o problema era free em um ponteiro que não era alocado. Esse chamado foi um dos mais difíceis que já tive até hoje, pois ao olhar os logs, a aplicação não morria no ponto onde havia o free na variável, mas morria em outro local, isso é muito comum em softwares grandes quando tem vazamento de memória, às vezes o software não morre no local onde tem o problema, o que dificulta a análise do problema.

Acredito que toda linguagem de programação tem benefícios, porém junto com eles alguns problemas, mas não acredito que o trabalho como desenvolvedor C ou C++ irá sumir tão cedo, existem muitos sistemas por aí usando essas linguagens e que provavelmente não irão migrar para linguagens mais modernas.

Não acho que a solução seja abandonar a linguagem C para outra, mas creio que devemos aprender a desenvolver com mais cautela. Muitas pessoas hoje em dia gostam de aprender com linguagens mais simples, o que pode gerar desenvolvedores com pouco conhecimento e entendimento do que estão fazendo. Creio que aprender C/C++ seja algo importante, mesmo que você não vá trabalhar usando elas.

3. Re: Qual é a melhor forma de utilizar ponteiros?

Samuel Leonardo
SamL

(usa XUbuntu)

Enviado em 08/03/2026 - 19:46h

O uso mais básico de ponteiros é quando vc tem uma quantidade de dados e precisa passar pra funções MAS não quer copiar os dados como parâmetro da função:
struct DadosDeExemplo {
char nome[2048];
};
//suponha agora que vc tem a seguinte função que retorna um struct DadosDeExemplo:
struct DadosDeExemplo lerDados() {
struct DadosDeExemplo dados;
printf("Digite o nome completo de dom Pedro I:\n");
fgets(dados.nome, 2048, stdin);
return dados;
}
int main() {
struct DadosDeExemplo dados;
dados = lerDados();
return 0;
}


A função lerDados é altamente ineficiente, sabe por que? Porque ela retorna uma cópia da struct DadosDeExemplo!
Apesar da função ser sintaticamente e semanticamente correta, ela é muito ineficiente porque ao chamar a lerDados, é alocada memória pra "dados" DENTRO dela e em seguida, os dados são populados e por fim, retornada OUTRA CÓPIA da variável dados!

Percebeu como é lenta a parada?

Um jeito de resolver isso é passando um ponteiro para então o ponteiro ser populado indiretamente, vou modificar, veja abaixo:
struct DadosDeExemplo {
char nome[2048];
};
//suponha agora que vc tem a seguinte função modificada que apenas modifica o parametro dados
void lerDados(struct DadosDeExemplo *dados) {
//aqui dentro, dados é uma área de memoria FORA de lerDados!
printf("Digite o nome completo de dom Pedro I:\n");
fgets(dados->nome, 2048, stdin);
}
int main() {
//cria a vairável local
struct DadosDeExemplo dados;
//passa o endereço de dados para lerDados
lerDados(&dados);
//aqui os dados estão populados
return 0;
}


E qual a diferença de um pro outro? Basicamente vc evitou duas cópias de uma struct gigante como a "DadosDeExemplo"!
Ou seja, seu código passou ficar ultra otimizado! Cria só uma vez os dados e ai popula ele só uma vez também.

Esse é o uso mais básico de ponteiros: fazer referência indireta para dados pré alocados pra evitar ter de copiá-los.

E quando utilizar ponteiros?
Vc usa quando TEM de evitar copiar dados para funções ou quando vc precisa que uma função modifique um dado externo DENTRO dela, como o caso acima.



______________________________
https://tutorpro-sam.blogspot.com/ acessa ai, é grátis!


4. Re: Qual é a melhor forma de utilizar ponteiros?

Gustavo Samuel Bacagine Azevedo
Bacagine

(usa Arch Linux)

Enviado em 09/03/2026 - 11:47h

Sou desenvolvedor C há 7 anos, e em todos esses anos, duas maneiras de usar ponteiros me chamaram atenção.

Os conceitos a seguir são avançados, algumas coisas que vou falar não são abordadas muito em livros ou em cursos por aí, então não se preocupe se tiver dificuldades em entender.

1) Ponteiros Genéricos (void*)
Os ponteiros genéricos (void pointers) são usados quando você não sabe qual tipo de dado vai ser usado. Imagine que você cria uma estrutura de lista, e ao invés de definir qual dado será salvo na lista, você cria uma lista com ponteiro genérico, assim ela passa a aceitar qualquer tipo de dado.


typedef struct STRUCT_GENERIC_LIST {
void* pData;
struct STRUCT_GENERIC_LIST* pstNext;
} STRUCT_GENERIC_LIST, *STRUCT_GENERIC_LIST;


2) Callbacks (ponteiros para funções)
Esse é um dos usos que eu mais vejo no dia a dia, trabalhando como desenvolvedor C na empresa onde estou.
Imagine que você está desenvolvendo uma biblioteca que tem uma função que precisa chamar outra, porém você não sabe o conteúdo dessa função que você vai chamar, você não tem essa função no seu código, a única coisa que você sabe é a assinatura que essa função tem. Nesse caso, você cria uma callback, um ponteiro para a função, assim quando um desenvolvedor usar sua biblioteca, ele irá passar a função como parâmetro para este ponteiro.
Veja um exemplo a seguir:


typedef int (*PFNCALLBACK)(void);
void vMyFunction(PFNCALLBACK pfnCallback);


1) Criamos um tipo de dado que é um ponteiro de função chamado PFNCALLBACK.
2) Criamos uma função que recebe como parâmetro uma função com a assinatura de PFNCALLBACK, ou seja, uma função que retorna um inteiro e não recebe parâmetros.

O desenvolvedor que for chamar a função vMyFunction terá que passar como parâmetro uma função que tenha essa assinatura. Exemplo:


int iHelloWorld(void) {
puts("Hello World!!!");
puts("I'm a callback :)");
return 1;
}

int main(void) {
vMyFunction(iHelloWorld);
return 0;
}



5. Re: Qual é a melhor forma de utilizar ponteiros? [RESOLVIDO]

J4ne D0bbs
j4ned0bbs

(usa Slackware)

Enviado em 09/03/2026 - 15:33h


Bacagine escreveu:

Sou desenvolvedor C há 7 anos, e em todos esses anos, duas maneiras de usar ponteiros me chamaram atenção.

Os conceitos a seguir são avançados, algumas coisas que vou falar não são abordadas muito em livros ou em cursos por aí, então não se preocupe se tiver dificuldades em entender.

1) Ponteiros Genéricos (void*)
Os ponteiros genéricos (void pointers) são usados quando você não sabe qual tipo de dado vai ser usado. Imagine que você cria uma estrutura de lista, e ao invés de definir qual dado será salvo na lista, você cria uma lista com ponteiro genérico, assim ela passa a aceitar qualquer tipo de dado.


typedef struct STRUCT_GENERIC_LIST {
void* pData;
struct STRUCT_GENERIC_LIST* pstNext;
} STRUCT_GENERIC_LIST, *STRUCT_GENERIC_LIST;


2) Callbacks (ponteiros para funções)
Esse é um dos usos que eu mais vejo no dia a dia, trabalhando como desenvolvedor C na empresa onde estou.
Imagine que você está desenvolvendo uma biblioteca que tem uma função que precisa chamar outra, porém você não sabe o conteúdo dessa função que você vai chamar, você não tem essa função no seu código, a única coisa que você sabe é a assinatura que essa função tem. Nesse caso, você cria uma callback, um ponteiro para a função, assim quando um desenvolvedor usar sua biblioteca, ele irá passar a função como parâmetro para este ponteiro.
Veja um exemplo a seguir:


typedef int (*PFNCALLBACK)(void);
void vMyFunction(PFNCALLBACK pfnCallback);


1) Criamos um tipo de dado que é um ponteiro de função chamado PFNCALLBACK.
2) Criamos uma função que recebe como parâmetro uma função com a assinatura de PFNCALLBACK, ou seja, uma função que retorna um inteiro e não recebe parâmetros.

O desenvolvedor que for chamar a função vMyFunction terá que passar como parâmetro uma função que tenha essa assinatura. Exemplo:


int iHelloWorld(void) {
puts("Hello World!!!");
puts("I'm a callback :)");
return 1;
}

int main(void) {
vMyFunction(iHelloWorld);
return 0;
}


Caraca, essa ultima parte sobre Callbacks explodiu a minha cabeça! É genial.

Obrigada pela sua resposta eu gostei bastante e abriu a minha mente para os ponteiros. Só fiquei com pequenas duvidas e curiosidades:
Minha primeira duvida é o quanto um dado "Void" ocupa de bytes de memoria, por curiosidade e também por que eu queria entender como o compilador vai aceitar outros tipos de dados naquele ponteiro Void, como inteiros e caracteres sendo que eles tem tamanhos maiores e diferentes.
Outra curiosidade é, se me permite perguntar, como é trabalhar com C? Eu fiquei curiosa por que achei que empregos de programação em C fossem sumir por conta daquele borborinho todo de "linguagens vulneráveis".

Please, keep it simple!


6. Re: Qual é a melhor forma de utilizar ponteiros? [RESOLVIDO]

J4ne D0bbs
j4ned0bbs

(usa Slackware)

Enviado em 09/03/2026 - 15:36h


SamL escreveu:

O uso mais básico de ponteiros é quando vc tem uma quantidade de dados e precisa passar pra funções MAS não quer copiar os dados como parâmetro da função:
struct DadosDeExemplo {
char nome[2048];
};
//suponha agora que vc tem a seguinte função que retorna um struct DadosDeExemplo:
struct DadosDeExemplo lerDados() {
struct DadosDeExemplo dados;
printf("Digite o nome completo de dom Pedro I:\n");
fgets(dados.nome, 2048, stdin);
return dados;
}
int main() {
struct DadosDeExemplo dados;
dados = lerDados();
return 0;
}


A função lerDados é altamente ineficiente, sabe por que? Porque ela retorna uma cópia da struct DadosDeExemplo!
Apesar da função ser sintaticamente e semanticamente correta, ela é muito ineficiente porque ao chamar a lerDados, é alocada memória pra "dados" DENTRO dela e em seguida, os dados são populados e por fim, retornada OUTRA CÓPIA da variável dados!

Percebeu como é lenta a parada?

Um jeito de resolver isso é passando um ponteiro para então o ponteiro ser populado indiretamente, vou modificar, veja abaixo:
struct DadosDeExemplo {
char nome[2048];
};
//suponha agora que vc tem a seguinte função modificada que apenas modifica o parametro dados
void lerDados(struct DadosDeExemplo *dados) {
//aqui dentro, dados é uma área de memoria FORA de lerDados!
printf("Digite o nome completo de dom Pedro I:\n");
fgets(dados->nome, 2048, stdin);
}
int main() {
//cria a vairável local
struct DadosDeExemplo dados;
//passa o endereço de dados para lerDados
lerDados(&dados);
//aqui os dados estão populados
return 0;
}


E qual a diferença de um pro outro? Basicamente vc evitou duas cópias de uma struct gigante como a "DadosDeExemplo"!
Ou seja, seu código passou ficar ultra otimizado! Cria só uma vez os dados e ai popula ele só uma vez também.

Esse é o uso mais básico de ponteiros: fazer referência indireta para dados pré alocados pra evitar ter de copiá-los.

E quando utilizar ponteiros?
Vc usa quando TEM de evitar copiar dados para funções ou quando vc precisa que uma função modifique um dado externo DENTRO dela, como o caso acima.



______________________________
https://tutorpro-sam.blogspot.com/ acessa ai, é grátis!


Obrigada pela resposta, olhando agora eu deveria ter usado mais os ponteiros ksksksk.

Uma outra duvida: E sobre os ponteiros que apontam para ponteiros? há alguma aplicação que precisaria ter um ponteiro apontando para outro ponteiro ou é algo bem raro?

Please, keep it simple!


7. Re: Qual é a melhor forma de utilizar ponteiros? [RESOLVIDO]

Samuel Leonardo
SamL

(usa XUbuntu)

Enviado em 09/03/2026 - 22:55h


j4ned0bbs escreveu:


SamL escreveu:

O uso mais básico de ponteiros é quando vc tem uma quantidade de dados e precisa passar pra funções MAS não quer copiar os dados como parâmetro da função:
struct DadosDeExemplo {
char nome[2048];
};
//suponha agora que vc tem a seguinte função que retorna um struct DadosDeExemplo:
struct DadosDeExemplo lerDados() {
struct DadosDeExemplo dados;
printf("Digite o nome completo de dom Pedro I:\n");
fgets(dados.nome, 2048, stdin);
return dados;
}
int main() {
struct DadosDeExemplo dados;
dados = lerDados();
return 0;
}


A função lerDados é altamente ineficiente, sabe por que? Porque ela retorna uma cópia da struct DadosDeExemplo!
Apesar da função ser sintaticamente e semanticamente correta, ela é muito ineficiente porque ao chamar a lerDados, é alocada memória pra "dados" DENTRO dela e em seguida, os dados são populados e por fim, retornada OUTRA CÓPIA da variável dados!

Percebeu como é lenta a parada?

Um jeito de resolver isso é passando um ponteiro para então o ponteiro ser populado indiretamente, vou modificar, veja abaixo:
struct DadosDeExemplo {
char nome[2048];
};
//suponha agora que vc tem a seguinte função modificada que apenas modifica o parametro dados
void lerDados(struct DadosDeExemplo *dados) {
//aqui dentro, dados é uma área de memoria FORA de lerDados!
printf("Digite o nome completo de dom Pedro I:\n");
fgets(dados->nome, 2048, stdin);
}
int main() {
//cria a vairável local
struct DadosDeExemplo dados;
//passa o endereço de dados para lerDados
lerDados(&dados);
//aqui os dados estão populados
return 0;
}


E qual a diferença de um pro outro? Basicamente vc evitou duas cópias de uma struct gigante como a "DadosDeExemplo"!
Ou seja, seu código passou ficar ultra otimizado! Cria só uma vez os dados e ai popula ele só uma vez também.

Esse é o uso mais básico de ponteiros: fazer referência indireta para dados pré alocados pra evitar ter de copiá-los.

E quando utilizar ponteiros?
Vc usa quando TEM de evitar copiar dados para funções ou quando vc precisa que uma função modifique um dado externo DENTRO dela, como o caso acima.



______________________________
https://tutorpro-sam.blogspot.com/ acessa ai, é grátis!


Obrigada pela resposta, olhando agora eu deveria ter usado mais os ponteiros ksksksk.

Uma outra duvida: E sobre os ponteiros que apontam para ponteiros? há alguma aplicação que precisaria ter um ponteiro apontando para outro ponteiro ou é algo bem raro?

Please, keep it simple!

Ponteiro para ponteiro pode ser visto a grosso modo como uma matriz.
Quando vc tem algo como isso:
int **mapa = criarMapa("./txt/labirinto.txt"); 

Isso significa que a variável mapa é um ponteiro para ponteiro e vc pode acessar os elementos dele assim: mapa[0][0], ou mapa[3 * 3][6], etc, desde que os valores inteiros nos índices estejam sempre dentro dos limites da matriz.
Observe também que a função criarMapa implica que dentro dela a matriz mapa é alocada dinamicamente e ai a função retorna o ponteiro para ponteiro e que é salvo em "mapa".
Mas isso não significa que foi copiado e sim que somente o endereço da memória foi retornado.

Veja aqui um código de exemplo que eu fiz no passado:
https://www.vivaolinux.com.br/script/Jogo-do-Labirinto-no-Terminal/
Com ele vc pode ver na prática como é o uso de diversos comandos.

Tem esse outro também:
https://www.vivaolinux.com.br/script/Agenda-Telefonica-Simples-em-C/


______________________________
https://tutorpro-sam.blogspot.com/ acessa ai, é grátis!






Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts